]> oss.titaniummirror.com Git - webber.git/commitdiff
used Emacs' "tabify" command on some python file.
authorHolger Schurig <holgerschurig@gmail.com>
Thu, 24 Jun 2010 07:06:05 +0000 (09:06 +0200)
committerHolger Schurig <holgerschurig@gmail.com>
Thu, 24 Jun 2010 07:06:05 +0000 (09:06 +0200)
By using http://www.emacswiki.org/emacs-en/SmartTabs together with a
tab-width of 4 you get a nice havior and a pleasing look.

plugins/link.py
plugins/read_markdown.py
plugins/rss_feed.py
plugins/toc.py
webber.py

index b0d673b306991beb4fcb17bec4940aaa8b105a07..6b57e256b560f5fe5a220b94dc8f892e7451d345 100644 (file)
@@ -5,18 +5,18 @@ import os, re
 # To understand this beast, read /usr/share/doc/python2.5-doc/html/lib/module-re.html :-)
 
 reLink = re.compile(r'''
 # To understand this beast, read /usr/share/doc/python2.5-doc/html/lib/module-re.html :-)
 
 reLink = re.compile(r'''
-       \[\[                # Begin of link
-       (?=[^!])            # Don't fire for macros
+       \[\[                            # Begin of link
+       (?=[^!])                        # Don't fire for macros
        (?:
        (?:
-               ([^\]\|]+)      # 1: link text
-               \|              # followed by '|'
-       )?                  # optional
-       ([^\n\r\]#]+)       # 2: page to link to
+               ([^\]\|]+)              # 1: link text
+               \|                              # followed by '|'
+       )?                                      # optional
+       ([^\n\r\]#]+)           # 2: page to link to
        (
        (
-               \#              # '#', beginning of anchor
-               [^\s\]]+        # 3: anchor text, doesn't contain spaces or ']'
-       )?                  # optional
-       \]\]                # end of link
+               \#                              # '#', beginning of anchor
+               [^\s\]]+                # 3: anchor text, doesn't contain spaces or ']'
+       )?                                      # optional
+       \]\]                            # end of link
        ''', re.VERBOSE)
 
 def do_link(m):
        ''', re.VERBOSE)
 
 def do_link(m):
@@ -55,9 +55,9 @@ def test_link():
                m = reLink.search(s)
                if m:
                        print "link:", s
                m = reLink.search(s)
                if m:
                        print "link:", s
-                       print "  name:", m.group(1)
-                       print "  link:", m.group(2)
-                       print "  anchor:", m.group(3)
+                       print "  name:", m.group(1)
+                       print "  link:", m.group(2)
+                       print "  anchor:", m.group(3)
                else:
                        print "No link:", s
 
                else:
                        print "No link:", s
 
index fead97eb97a1bfdbda01bf22cbc4e95fae94ae3d..d8006a27474c42e9eff7902c354f47f9f656013c 100644 (file)
@@ -15,9 +15,9 @@ from webber import *
 
 import os, sys, re, codecs
 try:
 
 import os, sys, re, codecs
 try:
-    from hashlib import md5
+       from hashlib import md5
 except ImportError:
 except ImportError:
-    from md5 import md5
+       from md5 import md5
 from random import random, randint
 
 
 from random import random, randint
 
 
@@ -25,15 +25,15 @@ from random import random, randint
 #---- Python version compat
 
 if sys.version_info[:2] < (2,4):
 #---- Python version compat
 
 if sys.version_info[:2] < (2,4):
-    from sets import Set as set
-    def reversed(sequence):
-        for i in sequence[::-1]:
-            yield i
-    def _unicode_decode(s, encoding, errors='xmlcharrefreplace'):
-        return unicode(s, encoding, errors)
+       from sets import Set as set
+       def reversed(sequence):
+               for i in sequence[::-1]:
+                       yield i
+       def _unicode_decode(s, encoding, errors='xmlcharrefreplace'):
+               return unicode(s, encoding, errors)
 else:
 else:
-    def _unicode_decode(s, encoding, errors='strict'):
-        return s.decode(encoding, errors)
+       def _unicode_decode(s, encoding, errors='strict'):
+               return s.decode(encoding, errors)
 
 
 #---- globals
 
 
 #---- globals
@@ -44,1464 +44,1464 @@ DEFAULT_TAB_WIDTH = 4
 
 
 try:
 
 
 try:
-    import uuid
+       import uuid
 except ImportError:
 except ImportError:
-    SECRET_SALT = str(randint(0, 1000000))
+       SECRET_SALT = str(randint(0, 1000000))
 else:
 else:
-    SECRET_SALT = str(uuid.uuid4())
+       SECRET_SALT = str(uuid.uuid4())
 def _hash_ascii(s):
 def _hash_ascii(s):
-    #return md5(s).hexdigest()   # Markdown.pl effectively does this.
-    return 'md5-' + md5(SECRET_SALT + s).hexdigest()
+       #return md5(s).hexdigest()       # Markdown.pl effectively does this.
+       return 'md5-' + md5(SECRET_SALT + s).hexdigest()
 def _hash_text(s):
 def _hash_text(s):
-    return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest()
+       return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest()
 
 # Table of hash values for escaped characters:
 g_escape_table = dict([(ch, _hash_ascii(ch))
 
 # Table of hash values for escaped characters:
 g_escape_table = dict([(ch, _hash_ascii(ch))
-                       for ch in '\\`*_{}[]()>#+-.!'])
+                                          for ch in '\\`*_{}[]()>#+-.!'])
 
 
 
 #---- exceptions
 
 class MarkdownError(Exception):
 
 
 
 #---- exceptions
 
 class MarkdownError(Exception):
-    pass
+       pass
 
 
 
 #---- public api
 
 def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
 
 
 
 #---- public api
 
 def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
-             safe_mode=None, extras=None, link_patterns=None):
-    return Markdown(html4tags=html4tags, tab_width=tab_width,
-                    safe_mode=safe_mode, extras=extras,
-                    link_patterns=link_patterns).convert(text)
+                        safe_mode=None, extras=None, link_patterns=None):
+       return Markdown(html4tags=html4tags, tab_width=tab_width,
+                                       safe_mode=safe_mode, extras=extras,
+                                       link_patterns=link_patterns).convert(text)
 
 class Markdown(object):
 
 class Markdown(object):
-    # The dict of "extras" to enable in processing -- a mapping of
-    # extra name to argument for the extra. Most extras do not have an
-    # argument, in which case the value is None.
-    #
-    # This can be set via (a) subclassing and (b) the constructor
-    # "extras" argument.
-    extras = None
-
-    urls = None
-    titles = None
-    html_blocks = None
-    html_spans = None
-    html_removed_text = "[HTML_REMOVED]"  # for compat with markdown.py
-
-    # Used to track when we're inside an ordered or unordered list
-    # (see _ProcessListItems() for details):
-    list_level = 0
-
-    _ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
-
-    def __init__(self, html4tags=False, tab_width=4, safe_mode=None,
-                 extras=None, link_patterns=None):
-        if html4tags:
-            self.empty_element_suffix = ">"
-        else:
-            self.empty_element_suffix = " />"
-        self.tab_width = tab_width
-
-        # For compatibility with earlier markdown2.py and with
-        # markdown.py's safe_mode being a boolean,
-        #   safe_mode == True -> "replace"
-        if safe_mode is True:
-            self.safe_mode = "replace"
-        else:
-            self.safe_mode = safe_mode
-
-        if self.extras is None:
-            self.extras = {}
-        elif not isinstance(self.extras, dict):
-            self.extras = dict([(e, None) for e in self.extras])
-        if extras:
-            if not isinstance(extras, dict):
-                extras = dict([(e, None) for e in extras])
-            self.extras.update(extras)
-        assert isinstance(self.extras, dict)
-        self._instance_extras = self.extras.copy()
-        self.link_patterns = link_patterns
-        self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
-
-    def reset(self):
-        self.urls = {}
-        self.titles = {}
-        self.html_blocks = {}
-        self.html_spans = {}
-        self.list_level = 0
-        self.extras = self._instance_extras.copy()
-        self.encoding = 'utf-8'
-        if "footnotes" in self.extras:
-            self.footnotes = {}
-            self.footnote_ids = []
-
-    def convert(self, text, encoding=None):
-        """Convert the given text."""
-        # Main function. The order in which other subs are called here is
-        # essential. Link and image substitutions need to happen before
-        # _EscapeSpecialChars(), so that any *'s or _'s in the <a>
-        # and <img> tags get encoded.
-
-        # Clear the global hashes. If we don't clear these, you get conflicts
-        # from other articles when generating a page which contains more than
-        # one article (e.g. an index page that shows the N most recent
-        # articles):
-        self.reset()
-        if encoding:
-            self.encoding = encoding
-
-        if not isinstance(text, unicode):
-            text = unicode(text, self.encoding)
-
-        # Standardize line endings:
-        #text = re.sub("\r\n|\r", "\n", text)
-
-        # Make sure $text ends with a couple of newlines:
-        text += "\n\n"
-
-        # Convert all tabs to spaces.
-        text = self._detab(text)
-
-        # Strip any lines consisting only of spaces and tabs.
-        # This makes subsequent regexen easier to write, because we can
-        # match consecutive blank lines with /\n+/ instead of something
-        # contorted like /[ \t]*\n+/ .
-        text = self._ws_only_line_re.sub("", text)
-
-        if self.safe_mode:
-            text = self._hash_html_spans(text)
-
-        # Turn block-level HTML blocks into hash entries
-        text = self._hash_html_blocks(text, raw=True)
-
-        # Strip link definitions, store in hashes.
-        if "footnotes" in self.extras:
-            # Must do footnotes first because an unlucky footnote defn
-            # looks like a link defn:
-            #   [^4]: this "looks like a link defn"
-            text = self._strip_footnote_definitions(text)
-        text = self._strip_link_definitions(text)
-
-        text = self._run_block_gamut(text)
-
-        if "footnotes" in self.extras:
-            text = self._add_footnotes(text)
-
-        text = self._unescape_special_chars(text)
-
-        if self.safe_mode:
-            text = self._unhash_html_spans(text)
-
-        text += "\n"
-        return text
-
-    # Cribbed from a post by Bart Lateur:
-    # <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>
-    _detab_re = re.compile(r'(.*?)\t', re.M)
-    def _detab_sub(self, match):
-        g1 = match.group(1)
-        return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width))
-    def _detab(self, text):
-        r"""Remove (leading?) tabs from a file.
-
-            >>> m = Markdown()
-            >>> m._detab("\tfoo")
-            '    foo'
-            >>> m._detab("  \tfoo")
-            '    foo'
-            >>> m._detab("\t  foo")
-            '      foo'
-            >>> m._detab("  foo")
-            '  foo'
-            >>> m._detab("  foo\n\tbar\tblam")
-            '  foo\n    bar blam'
-        """
-        if '\t' not in text:
-            return text
-        return self._detab_re.subn(self._detab_sub, text)[0]
-
-    _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del'
-    _strict_tag_block_re = re.compile(r"""
-        (                       # save in \1
-            ^                   # start of line  (with re.M)
-            <(%s)               # start tag = \2
-            \b                  # word break
-            (.*\n)*?            # any number of lines, minimally matching
-            </\2>               # the matching end tag
-            [ \t]*              # trailing spaces/tabs
-            (?=\n+|\Z)          # followed by a newline or end of document
-        )
-        """ % _block_tags_a,
-        re.X | re.M)
-
-    _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math'
-    _liberal_tag_block_re = re.compile(r"""
-        (                       # save in \1
-            ^                   # start of line  (with re.M)
-            <(%s)               # start tag = \2
-            \b                  # word break
-            (.*\n)*?            # any number of lines, minimally matching
-            .*</\2>             # the matching end tag
-            [ \t]*              # trailing spaces/tabs
-            (?=\n+|\Z)          # followed by a newline or end of document
-        )
-        """ % _block_tags_b,
-        re.X | re.M)
-
-    def _hash_html_block_sub(self, match, raw=False):
-        html = match.group(1)
-        if raw and self.safe_mode:
-            html = self._sanitize_html(html)
-        key = _hash_text(html)
-        self.html_blocks[key] = html
-        return "\n\n" + key + "\n\n"
-
-    def _hash_html_blocks(self, text, raw=False):
-        """Hashify HTML blocks
-
-        We only want to do this for block-level HTML tags, such as headers,
-        lists, and tables. That's because we still want to wrap <p>s around
-        "paragraphs" that are wrapped in non-block-level tags, such as anchors,
-        phrase emphasis, and spans. The list of tags we're looking for is
-        hard-coded.
-
-        @param raw {boolean} indicates if these are raw HTML blocks in
-            the original source. It makes a difference in "safe" mode.
-        """
-        if '<' not in text:
-            return text
-
-        # Pass `raw` value into our calls to self._hash_html_block_sub.
-        hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw)
-
-        # First, look for nested blocks, e.g.:
-        #   <div>
-        #       <div>
-        #       tags for inner block must be indented.
-        #       </div>
-        #   </div>
-        #
-        # The outermost tags must start at the left margin for this to match, and
-        # the inner nested divs must be indented.
-        # We need to do this before the next, more liberal match, because the next
-        # match will start at the first `<div>` and stop at the first `</div>`.
-        text = self._strict_tag_block_re.sub(hash_html_block_sub, text)
-
-        # Now match more liberally, simply from `\n<tag>` to `</tag>\n`
-        text = self._liberal_tag_block_re.sub(hash_html_block_sub, text)
-
-        # Special case just for <hr />. It was easier to make a special
-        # case than to make the other regex more complicated.
-        if "<hr" in text:
-            _hr_tag_re = _hr_tag_re_from_tab_width(self.tab_width)
-            text = _hr_tag_re.sub(hash_html_block_sub, text)
-
-        # Special case for standalone HTML comments:
-        if "<!--" in text:
-            start = 0
-            while True:
-                # Delimiters for next comment block.
-                try:
-                    start_idx = text.index("<!--", start)
-                except ValueError, ex:
-                    break
-                try:
-                    end_idx = text.index("-->", start_idx) + 3
-                except ValueError, ex:
-                    break
-
-                # Start position for next comment block search.
-                start = end_idx
-
-                # Validate whitespace before comment.
-                if start_idx:
-                    # - Up to `tab_width - 1` spaces before start_idx.
-                    for i in range(self.tab_width - 1):
-                        if text[start_idx - 1] != ' ':
-                            break
-                        start_idx -= 1
-                        if start_idx == 0:
-                            break
-                    # - Must be preceded by 2 newlines or hit the start of
-                    #   the document.
-                    if start_idx == 0:
-                        pass
-                    elif start_idx == 1 and text[0] == '\n':
-                        start_idx = 0  # to match minute detail of Markdown.pl regex
-                    elif text[start_idx-2:start_idx] == '\n\n':
-                        pass
-                    else:
-                        break
-
-                # Validate whitespace after comment.
-                # - Any number of spaces and tabs.
-                while end_idx < len(text):
-                    if text[end_idx] not in ' \t':
-                        break
-                    end_idx += 1
-                # - Must be following by 2 newlines or hit end of text.
-                if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'):
-                    continue
-
-                # Escape and hash (must match `_hash_html_block_sub`).
-                html = text[start_idx:end_idx]
-                if raw and self.safe_mode:
-                    html = self._sanitize_html(html)
-                key = _hash_text(html)
-                self.html_blocks[key] = html
-                text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:]
-
-        if "xml" in self.extras:
-            # Treat XML processing instructions and namespaced one-liner
-            # tags as if they were block HTML tags. E.g., if standalone
-            # (i.e. are their own paragraph), the following do not get
-            # wrapped in a <p> tag:
-            #    <?foo bar?>
-            #
-            #    <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="chapter_1.md"/>
-            _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width)
-            text = _xml_oneliner_re.sub(hash_html_block_sub, text)
-
-        return text
-
-    def _strip_link_definitions(self, text):
-        # Strips link definitions from text, stores the URLs and titles in
-        # hash references.
-        less_than_tab = self.tab_width - 1
-
-        # Link defs are in the form:
-        #   [id]: url "optional title"
-        _link_def_re = re.compile(r"""
-            ^[ ]{0,%d}\[(.+)\]: # id = \1
-              [ \t]*
-              \n?               # maybe *one* newline
-              [ \t]*
-            <?(.+?)>?           # url = \2
-              [ \t]*
-            (?:
-                \n?             # maybe one newline
-                [ \t]*
-                (?<=\s)         # lookbehind for whitespace
-                ['"(]
-                ([^\n]*)        # title = \3
-                ['")]
-                [ \t]*
-            )?  # title is optional
-            (?:\n+|\Z)
-            """ % less_than_tab, re.X | re.M | re.U)
-        return _link_def_re.sub(self._extract_link_def_sub, text)
-
-    def _extract_link_def_sub(self, match):
-        id, url, title = match.groups()
-        key = id.lower()    # Link IDs are case-insensitive
-        self.urls[key] = self._encode_amps_and_angles(url)
-        if title:
-            self.titles[key] = title.replace('"', '&quot;')
-        return ""
-
-    def _extract_footnote_def_sub(self, match):
-        id, text = match.groups()
-        text = _dedent(text, skip_first_line=not text.startswith('\n')).strip()
-        normed_id = re.sub(r'\W', '-', id)
-        # Ensure footnote text ends with a couple newlines (for some
-        # block gamut matches).
-        self.footnotes[normed_id] = text + "\n\n"
-        return ""
-
-    def _strip_footnote_definitions(self, text):
-        """A footnote definition looks like this:
-
-            [^note-id]: Text of the note.
-
-                May include one or more indented paragraphs.
-
-        Where,
-        - The 'note-id' can be pretty much anything, though typically it
-          is the number of the footnote.
-        - The first paragraph may start on the next line, like so:
-
-            [^note-id]:
-                Text of the note.
-        """
-        less_than_tab = self.tab_width - 1
-        footnote_def_re = re.compile(r'''
-            ^[ ]{0,%d}\[\^(.+)\]:   # id = \1
-            [ \t]*
-            (                       # footnote text = \2
-              # First line need not start with the spaces.
-              (?:\s*.*\n+)
-              (?:
-                (?:[ ]{%d} | \t)  # Subsequent lines must be indented.
-                .*\n+
-              )*
-            )
-            # Lookahead for non-space at line-start, or end of doc.
-            (?:(?=^[ ]{0,%d}\S)|\Z)
-            ''' % (less_than_tab, self.tab_width, self.tab_width),
-            re.X | re.M)
-        return footnote_def_re.sub(self._extract_footnote_def_sub, text)
-
-
-    _hr_res = [
-        re.compile(r"^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$", re.M),
-        re.compile(r"^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$", re.M),
-        re.compile(r"^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$", re.M),
-    ]
-
-    def _run_block_gamut(self, text):
-        # These are all the transformations that form block-level
-        # tags like paragraphs, headers, and list items.
-
-        text = self._do_headers(text)
-
-        # Do Horizontal Rules:
-        hr = "\n<hr"+self.empty_element_suffix+"\n"
-        for hr_re in self._hr_res:
-            text = hr_re.sub(hr, text)
-
-        text = self._do_lists(text)
-
-        if "pyshell" in self.extras:
-            text = self._prepare_pyshell_blocks(text)
-
-        text = self._do_code_blocks(text)
-
-        text = self._do_block_quotes(text)
-
-        # We already ran _HashHTMLBlocks() before, in Markdown(), but that
-        # was to escape raw HTML in the original Markdown source. This time,
-        # we're escaping the markup we've just created, so that we don't wrap
-        # <p> tags around block-level tags.
-        text = self._hash_html_blocks(text)
-
-        text = self._form_paragraphs(text)
-
-        return text
-
-    def _pyshell_block_sub(self, match):
-        lines = match.group(0).splitlines(0)
-        _dedentlines(lines)
-        indent = ' ' * self.tab_width
-        s = ('\n' # separate from possible cuddled paragraph
-             + indent + ('\n'+indent).join(lines)
-             + '\n\n')
-        return s
-
-    def _prepare_pyshell_blocks(self, text):
-        """Ensure that Python interactive shell sessions are put in
-        code blocks -- even if not properly indented.
-        """
-        if ">>>" not in text:
-            return text
-
-        less_than_tab = self.tab_width - 1
-        _pyshell_block_re = re.compile(r"""
-            ^([ ]{0,%d})>>>[ ].*\n   # first line
-            ^(\1.*\S+.*\n)*         # any number of subsequent lines
-            ^\n                     # ends with a blank line
-            """ % less_than_tab, re.M | re.X)
-
-        return _pyshell_block_re.sub(self._pyshell_block_sub, text)
-
-    def _run_span_gamut(self, text):
-        # These are all the transformations that occur *within* block-level
-        # tags like paragraphs, headers, and list items.
-
-        text = self._do_code_spans(text)
-
-        text = self._escape_special_chars(text)
-
-        # Process anchor and image tags.
-        #text = self._do_links(text)
-
-        # Make links out of things like `<http://example.com/>`
-        # Must come after _do_links(), because you can use < and >
-        # delimiters in inline links like [this](<url>).
-        text = self._do_auto_links(text)
-
-        if "link-patterns" in self.extras:
-            text = self._do_link_patterns(text)
-
-        text = self._encode_amps_and_angles(text)
-
-        text = self._do_italics_and_bold(text)
-
-        # Do hard breaks:
-        text = re.sub(r" {2,}\n", " <br%s\n" % self.empty_element_suffix, text)
-
-        return text
-
-    # "Sorta" because auto-links are identified as "tag" tokens.
-    _sorta_html_tokenize_re = re.compile(r"""
-        (
-            # tag
-            </?
-            (?:\w+)                                     # tag name
-            (?:\s+(?:[\w-]+:)?[\w-]+=(?:".*?"|'.*?'))*  # attributes
-            \s*/?>
-            |
-            # auto-link (e.g., <http://www.activestate.com/>)
-            <\w+[^>]*>
-            |
-            <!--.*?-->      # comment
-            |
-            <\?.*?\?>       # processing instruction
-        )
-        """, re.X)
-
-    def _escape_special_chars(self, text):
-        # Python markdown note: the HTML tokenization here differs from
-        # that in Markdown.pl, hence the behaviour for subtle cases can
-        # differ (I believe the tokenizer here does a better job because
-        # it isn't susceptible to unmatched '<' and '>' in HTML tags).
-        # Note, however, that '>' is not allowed in an auto-link URL
-        # here.
-        escaped = []
-        is_html_markup = False
-        for token in self._sorta_html_tokenize_re.split(text):
-            if is_html_markup:
-                # Within tags/HTML-comments/auto-links, encode * and _
-                # so they don't conflict with their use in Markdown for
-                # italics and strong.  We're replacing each such
-                # character with its corresponding MD5 checksum value;
-                # this is likely overkill, but it should prevent us from
-                # colliding with the escape values by accident.
-                escaped.append(token.replace('*', g_escape_table['*'])
-                                    .replace('_', g_escape_table['_']))
-            else:
-                escaped.append(self._encode_backslash_escapes(token))
-            is_html_markup = not is_html_markup
-        return ''.join(escaped)
-
-    def _hash_html_spans(self, text):
-        # Used for safe_mode.
-
-        def _is_auto_link(s):
-            if ':' in s and self._auto_link_re.match(s):
-                return True
-            elif '@' in s and self._auto_email_link_re.match(s):
-                return True
-            return False
-
-        tokens = []
-        is_html_markup = False
-        for token in self._sorta_html_tokenize_re.split(text):
-            if is_html_markup and not _is_auto_link(token):
-                sanitized = self._sanitize_html(token)
-                key = _hash_text(sanitized)
-                self.html_spans[key] = sanitized
-                tokens.append(key)
-            else:
-                tokens.append(token)
-            is_html_markup = not is_html_markup
-        return ''.join(tokens)
-
-    def _unhash_html_spans(self, text):
-        for key, sanitized in self.html_spans.items():
-            text = text.replace(key, sanitized)
-        return text
-
-    def _sanitize_html(self, s):
-        if self.safe_mode == "replace":
-            return self.html_removed_text
-        elif self.safe_mode == "escape":
-            replacements = [
-                ('&', '&amp;'),
-                ('<', '&lt;'),
-                ('>', '&gt;'),
-            ]
-            for before, after in replacements:
-                s = s.replace(before, after)
-            return s
-        else:
-            raise MarkdownError("invalid value for 'safe_mode': %r (must be "
-                                "'escape' or 'replace')" % self.safe_mode)
-
-    _tail_of_inline_link_re = re.compile(r'''
-          # Match tail of: [text](/url/) or [text](/url/ "title")
-          \(            # literal paren
-            [ \t]*
-            (?P<url>            # \1
-                <.*?>
-                |
-                .*?
-            )
-            [ \t]*
-            (                   # \2
-              (['"])            # quote char = \3
-              (?P<title>.*?)
-              \3                # matching quote
-            )?                  # title is optional
-          \)
-        ''', re.X | re.S)
-    _tail_of_reference_link_re = re.compile(r'''
-          # Match tail of: [text][id]
-          [ ]?          # one optional space
-          (?:\n[ ]*)?   # one optional newline followed by spaces
-          \[
-            (?P<id>.*?)
-          \]
-        ''', re.X | re.S)
-
-    def _do_links(self, text):
-        """Turn Markdown link shortcuts into XHTML <a> and <img> tags.
-
-        This is a combination of Markdown.pl's _DoAnchors() and
-        _DoImages(). They are done together because that simplified the
-        approach. It was necessary to use a different approach than
-        Markdown.pl because of the lack of atomic matching support in
-        Python's regex engine used in $g_nested_brackets.
-        """
-        MAX_LINK_TEXT_SENTINEL = 3000  # markdown2 issue 24
-
-        # `anchor_allowed_pos` is used to support img links inside
-        # anchors, but not anchors inside anchors. An anchor's start
-        # pos must be `>= anchor_allowed_pos`.
-        anchor_allowed_pos = 0
-
-        curr_pos = 0
-        while True: # Handle the next link.
-            # The next '[' is the start of:
-            # - an inline anchor:   [text](url "title")
-            # - a reference anchor: [text][id]
-            # - an inline img:      ![text](url "title")
-            # - a reference img:    ![text][id]
-            # - a footnote ref:     [^id]
-            #   (Only if 'footnotes' extra enabled)
-            # - a footnote defn:    [^id]: ...
-            #   (Only if 'footnotes' extra enabled) These have already
-            #   been stripped in _strip_footnote_definitions() so no
-            #   need to watch for them.
-            # - a link definition:  [id]: url "title"
-            #   These have already been stripped in
-            #   _strip_link_definitions() so no need to watch for them.
-            # - not markup:         [...anything else...
-            try:
-                start_idx = text.index('[', curr_pos)
-            except ValueError:
-                break
-            text_length = len(text)
-
-            # Find the matching closing ']'.
-            # Markdown.pl allows *matching* brackets in link text so we
-            # will here too. Markdown.pl *doesn't* currently allow
-            # matching brackets in img alt text -- we'll differ in that
-            # regard.
-            bracket_depth = 0
-            for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL,
-                                            text_length)):
-                ch = text[p]
-                if ch == ']':
-                    bracket_depth -= 1
-                    if bracket_depth < 0:
-                        break
-                elif ch == '[':
-                    bracket_depth += 1
-            else:
-                # Closing bracket not found within sentinel length.
-                # This isn't markup.
-                curr_pos = start_idx + 1
-                continue
-            link_text = text[start_idx+1:p]
-
-            # Possibly a footnote ref?
-            if "footnotes" in self.extras and link_text.startswith("^"):
-                normed_id = re.sub(r'\W', '-', link_text[1:])
-                if normed_id in self.footnotes:
-                    self.footnote_ids.append(normed_id)
-                    result = '<sup class="footnote-ref" id="fnref-%s">' \
-                             '<a href="#fn-%s">%s</a></sup>' \
-                             % (normed_id, normed_id, len(self.footnote_ids))
-                    text = text[:start_idx] + result + text[p+1:]
-                else:
-                    # This id isn't defined, leave the markup alone.
-                    curr_pos = p+1
-                continue
-
-            # Now determine what this is by the remainder.
-            p += 1
-            if p == text_length:
-                return text
-
-            # Inline anchor or img?
-            if text[p] == '(': # attempt at perf improvement
-                match = self._tail_of_inline_link_re.match(text, p)
-                if match:
-                    # Handle an inline anchor or img.
-                    is_img = start_idx > 0 and text[start_idx-1] == "!"
-                    if is_img:
-                        start_idx -= 1
-
-                    url, title = match.group("url"), match.group("title")
-                    if url and url[0] == '<':
-                        url = url[1:-1]  # '<url>' -> 'url'
-                    # We've got to encode these to avoid conflicting
-                    # with italics/bold.
-                    url = url.replace('*', g_escape_table['*']) \
-                             .replace('_', g_escape_table['_'])
-                    if title:
-                        title_str = ' title="%s"' \
-                            % title.replace('*', g_escape_table['*']) \
-                                   .replace('_', g_escape_table['_']) \
-                                   .replace('"', '&quot;')
-                    else:
-                        title_str = ''
-                    if is_img:
-                        result = '<img src="%s" alt="%s"%s%s' \
-                            % (url.replace('"', '&quot;'),
-                               link_text.replace('"', '&quot;'),
-                               title_str, self.empty_element_suffix)
-                        curr_pos = start_idx + len(result)
-                        text = text[:start_idx] + result + text[match.end():]
-                    elif start_idx >= anchor_allowed_pos:
-                        result_head = '<a href="%s"%s>' % (url, title_str)
-                        result = '%s%s</a>' % (result_head, link_text)
-                        # <img> allowed from curr_pos on, <a> from
-                        # anchor_allowed_pos on.
-                        curr_pos = start_idx + len(result_head)
-                        anchor_allowed_pos = start_idx + len(result)
-                        text = text[:start_idx] + result + text[match.end():]
-                    else:
-                        # Anchor not allowed here.
-                        curr_pos = start_idx + 1
-                    continue
-
-            # Reference anchor or img?
-            else:
-                match = self._tail_of_reference_link_re.match(text, p)
-                if match:
-                    # Handle a reference-style anchor or img.
-                    is_img = start_idx > 0 and text[start_idx-1] == "!"
-                    if is_img:
-                        start_idx -= 1
-                    link_id = match.group("id").lower()
-                    if not link_id:
-                        link_id = link_text.lower()  # for links like [this][]
-                    if link_id in self.urls:
-                        url = self.urls[link_id]
-                        # We've got to encode these to avoid conflicting
-                        # with italics/bold.
-                        url = url.replace('*', g_escape_table['*']) \
-                                 .replace('_', g_escape_table['_'])
-                        title = self.titles.get(link_id)
-                        if title:
-                            title = title.replace('*', g_escape_table['*']) \
-                                         .replace('_', g_escape_table['_'])
-                            title_str = ' title="%s"' % title
-                        else:
-                            title_str = ''
-                        if is_img:
-                            result = '<img src="%s" alt="%s"%s%s' \
-                                % (url.replace('"', '&quot;'),
-                                   link_text.replace('"', '&quot;'),
-                                   title_str, self.empty_element_suffix)
-                            curr_pos = start_idx + len(result)
-                            text = text[:start_idx] + result + text[match.end():]
-                        elif start_idx >= anchor_allowed_pos:
-                            result = '<a href="%s"%s>%s</a>' \
-                                % (url, title_str, link_text)
-                            result_head = '<a href="%s"%s>' % (url, title_str)
-                            result = '%s%s</a>' % (result_head, link_text)
-                            # <img> allowed from curr_pos on, <a> from
-                            # anchor_allowed_pos on.
-                            curr_pos = start_idx + len(result_head)
-                            anchor_allowed_pos = start_idx + len(result)
-                            text = text[:start_idx] + result + text[match.end():]
-                        else:
-                            # Anchor not allowed here.
-                            curr_pos = start_idx + 1
-                    else:
-                        # This id isn't defined, leave the markup alone.
-                        curr_pos = match.end()
-                    continue
-
-            # Otherwise, it isn't markup.
-            curr_pos = start_idx + 1
-
-        return text
-
-
-    _setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M)
-    def _setext_h_sub(self, match):
-        n = {"=": 1, "-": 2}[match.group(2)[0]]
-        demote_headers = self.extras.get("demote-headers")
-        if demote_headers:
-            n = min(n + demote_headers, 6)
-        return "<h%d>%s</h%d>\n\n" \
-               % (n, self._run_span_gamut(match.group(1)), n)
-
-    _atx_h_re = re.compile(r'''
-        ^([\#=]{1,6})  # \1 = string of #'s
-        [ \t]*
-        (.+?)       # \2 = Header text
-        [ \t]*
-        (?<!\\)     # ensure not an escaped trailing '#'
-        [\#=]*      # optional closing #'s (not counted)
-        \n+
-        ''', re.X | re.M)
-    def _atx_h_sub(self, match):
-        n = len(match.group(1))
-        demote_headers = self.extras.get("demote-headers")
-        if demote_headers:
-            n = min(n + demote_headers, 6)
-        return "<h%d>%s</h%d>\n\n" \
-               % (n, self._run_span_gamut(match.group(2)), n)
-
-    def _do_headers(self, text):
-        # Setext-style headers:
-        #     Header 1
-        #     ========
-        #
-        #     Header 2
-        #     --------
-        text = self._setext_h_re.sub(self._setext_h_sub, text)
-
-        # atx-style headers:
-        #   # Header 1
-        #   ## Header 2
-        #   ## Header 2 with closing hashes ##
-        #   ...
-        #   ###### Header 6
-        text = self._atx_h_re.sub(self._atx_h_sub, text)
-
-        return text
-
-
-    _marker_ul_chars  = '*+-'
-    _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars
-    _marker_ul = '(?:[%s])' % _marker_ul_chars
-    _marker_ol = r'(?:\d+\.)'
-
-    def _list_sub(self, match):
-        lst = match.group(1)
-        lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol"
-        result = self._process_list_items(lst)
-        if self.list_level:
-            return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type)
-        else:
-            return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type)
-
-    def _do_lists(self, text):
-        # Form HTML ordered (numbered) and unordered (bulleted) lists.
-
-        for marker_pat in (self._marker_ul, self._marker_ol):
-            # Re-usable pattern to match any entire ul or ol list:
-            less_than_tab = self.tab_width - 1
-            whole_list = r'''
-                (                   # \1 = whole list
-                  (                 # \2
-                    [ ]{0,%d}
-                    (%s)            # \3 = first list item marker
-                    [ \t]+
-                  )
-                  (?:.+?)
-                  (                 # \4
-                      \Z
-                    |
-                      \n{2,}
-                      (?=\S)
-                      (?!           # Negative lookahead for another list item marker
-                        [ \t]*
-                        %s[ \t]+
-                      )
-                  )
-                )
-            ''' % (less_than_tab, marker_pat, marker_pat)
-
-            # We use a different prefix before nested lists than top-level lists.
-            # See extended comment in _process_list_items().
-            #
-            # Note: There's a bit of duplication here. My original implementation
-            # created a scalar regex pattern as the conditional result of the test on
-            # $g_list_level, and then only ran the $text =~ s{...}{...}egmx
-            # substitution once, using the scalar as the pattern. This worked,
-            # everywhere except when running under MT on my hosting account at Pair
-            # Networks. There, this caused all rebuilds to be killed by the reaper (or
-            # perhaps they crashed, but that seems incredibly unlikely given that the
-            # same script on the same server ran fine *except* under MT. I've spent
-            # more time trying to figure out why this is happening than I'd like to
-            # admit. My only guess, backed up by the fact that this workaround works,
-            # is that Perl optimizes the substition when it can figure out that the
-            # pattern will never change, and when this optimization isn't on, we run
-            # afoul of the reaper. Thus, the slightly redundant code to that uses two
-            # static s/// patterns rather than one conditional pattern.
-
-            if self.list_level:
-                sub_list_re = re.compile("^"+whole_list, re.X | re.M | re.S)
-                text = sub_list_re.sub(self._list_sub, text)
-            else:
-                list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list,
-                                     re.X | re.M | re.S)
-                text = list_re.sub(self._list_sub, text)
-
-        return text
-
-    _list_item_re = re.compile(r'''
-        (\n)?               # leading line = \1
-        (^[ \t]*)           # leading whitespace = \2
-        (?P<marker>%s) [ \t]+   # list marker = \3
-        ((?:.+?)            # list item text = \4
-         (\n{1,2}))         # eols = \5
-        (?= \n* (\Z | \2 (?P<next_marker>%s) [ \t]+))
-        ''' % (_marker_any, _marker_any),
-        re.M | re.X | re.S)
-
-    _last_li_endswith_two_eols = False
-    def _list_item_sub(self, match):
-        item = match.group(4)
-        leading_line = match.group(1)
-        leading_space = match.group(2)
-        if leading_line or "\n\n" in item or self._last_li_endswith_two_eols:
-            item = self._run_block_gamut(self._outdent(item))
-        else:
-            # Recursion for sub-lists:
-            item = self._do_lists(self._outdent(item))
-            if item.endswith('\n'):
-                item = item[:-1]
-            item = self._run_span_gamut(item)
-        self._last_li_endswith_two_eols = (len(match.group(5)) == 2)
-        return "<li>%s</li>\n" % item
-
-    def _process_list_items(self, list_str):
-        # Process the contents of a single ordered or unordered list,
-        # splitting it into individual list items.
-
-        # The $g_list_level global keeps track of when we're inside a list.
-        # Each time we enter a list, we increment it; when we leave a list,
-        # we decrement. If it's zero, we're not in a list anymore.
-        #
-        # We do this because when we're not inside a list, we want to treat
-        # something like this:
-        #
-        #       I recommend upgrading to version
-        #       8. Oops, now this line is treated
-        #       as a sub-list.
-        #
-        # As a single paragraph, despite the fact that the second line starts
-        # with a digit-period-space sequence.
-        #
-        # Whereas when we're inside a list (or sub-list), that line will be
-        # treated as the start of a sub-list. What a kludge, huh? This is
-        # an aspect of Markdown's syntax that's hard to parse perfectly
-        # without resorting to mind-reading. Perhaps the solution is to
-        # change the syntax rules such that sub-lists must start with a
-        # starting cardinal number; e.g. "1." or "a.".
-        self.list_level += 1
-        self._last_li_endswith_two_eols = False
-        list_str = list_str.rstrip('\n') + '\n'
-        list_str = self._list_item_re.sub(self._list_item_sub, list_str)
-        self.list_level -= 1
-        return list_str
-
-    def _get_pygments_lexer(self, lexer_name):
-        try:
-            from pygments import lexers, util
-        except ImportError:
-            return None
-        try:
-            return lexers.get_lexer_by_name(lexer_name)
-        except util.ClassNotFound:
-            return None
-
-    def _color_with_pygments(self, codeblock, lexer, **formatter_opts):
-        import pygments
-        import pygments.formatters
-
-        class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
-            def _wrap_code(self, inner):
-                """A function for use in a Pygments Formatter which
-                wraps in <code> tags.
-                """
-                yield 0, "<code>"
-                for tup in inner:
-                    yield tup
-                yield 0, "</code>"
-
-            def wrap(self, source, outfile):
-                """Return the source with a code, pre, and div."""
-                return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
-
-        formatter = HtmlCodeFormatter(cssclass="codehilite", **formatter_opts)
-        return pygments.highlight(codeblock, lexer, formatter)
-
-    def _code_block_sub(self, match):
-        codeblock = match.group(1)
-        codeblock = self._outdent(codeblock)
-        codeblock = self._detab(codeblock)
-        codeblock = codeblock.lstrip('\n')  # trim leading newlines
-        codeblock = codeblock.rstrip()      # trim trailing whitespace
-
-        if "code-color" in self.extras and codeblock.startswith(":::"):
-            lexer_name, rest = codeblock.split('\n', 1)
-            lexer_name = lexer_name[3:].strip()
-            lexer = self._get_pygments_lexer(lexer_name)
-            codeblock = rest.lstrip("\n")   # Remove lexer declaration line.
-            if lexer:
-                formatter_opts = self.extras['code-color'] or {}
-                colored = self._color_with_pygments(codeblock, lexer,
-                                                    **formatter_opts)
-                return "\n\n%s\n\n" % colored
-
-        codeblock = self._encode_code(codeblock)
-        return "\n\n<pre><code>%s\n</code></pre>\n\n" % codeblock
-
-    def _do_code_blocks(self, text):
-        """Process Markdown `<pre><code>` blocks."""
-        code_block_re = re.compile(r'''
-            (?:\n\n|\A)
-            (               # $1 = the code block -- one or more lines, starting with a space/tab
-              (?:
-                (?:[ ]{%d} | \t)  # Lines must start with a tab or a tab-width of spaces
-                .*\n+
-              )+
-            )
-            ((?=^[ ]{0,%d}\S)|\Z)   # Lookahead for non-space at line-start, or end of doc
-            ''' % (self.tab_width, self.tab_width),
-            re.M | re.X)
-
-        return code_block_re.sub(self._code_block_sub, text)
-
-
-    # Rules for a code span:
-    # - backslash escapes are not interpreted in a code span
-    # - to include one or or a run of more backticks the delimiters must
-    #   be a longer run of backticks
-    # - cannot start or end a code span with a backtick; pad with a
-    #   space and that space will be removed in the emitted HTML
-    # See `test/tm-cases/escapes.text` for a number of edge-case
-    # examples.
-    _code_span_re = re.compile(r'''
-            (?<!\\)
-            (`+)        # \1 = Opening run of `
-            (?!`)       # See Note A test/tm-cases/escapes.text
-            (.+?)       # \2 = The code block
-            (?<!`)
-            \1          # Matching closer
-            (?!`)
-        ''', re.X | re.S)
-
-    def _code_span_sub(self, match):
-        c = match.group(2).strip(" \t")
-        c = self._encode_code(c)
-        return "<code>%s</code>" % c
-
-    def _do_code_spans(self, text):
-        #   *   Backtick quotes are used for <code></code> spans.
-        #
-        #   *   You can use multiple backticks as the delimiters if you want to
-        #       include literal backticks in the code span. So, this input:
-        #
-        #         Just type ``foo `bar` baz`` at the prompt.
-        #
-        #       Will translate to:
-        #
-        #         <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
-        #
-        #       There's no arbitrary limit to the number of backticks you
-        #       can use as delimters. If you need three consecutive backticks
-        #       in your code, use four for delimiters, etc.
-        #
-        #   *   You can use spaces to get literal backticks at the edges:
-        #
-        #         ... type `` `bar` `` ...
-        #
-        #       Turns to:
-        #
-        #         ... type <code>`bar`</code> ...
-        return self._code_span_re.sub(self._code_span_sub, text)
-
-    def _encode_code(self, text):
-        """Encode/escape certain characters inside Markdown code runs.
-        The point is that in code, these characters are literals,
-        and lose their special Markdown meanings.
-        """
-        replacements = [
-            # Encode all ampersands; HTML entities are not
-            # entities within a Markdown code span.
-            ('&', '&amp;'),
-            # Do the angle bracket song and dance:
-            ('<', '&lt;'),
-            ('>', '&gt;'),
-            # Now, escape characters that are magic in Markdown:
-            ('*', g_escape_table['*']),
-            ('_', g_escape_table['_']),
-            ('{', g_escape_table['{']),
-            ('}', g_escape_table['}']),
-            ('[', g_escape_table['[']),
-            (']', g_escape_table[']']),
-            ('\\', g_escape_table['\\']),
-        ]
-        for before, after in replacements:
-            text = text.replace(before, after)
-        return text
-
-    _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S)
-    _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S)
-    _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S)
-    _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S)
-    def _do_italics_and_bold(self, text):
-        # <strong> must go first:
-        if "code-friendly" in self.extras:
-            text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text)
-            text = self._code_friendly_em_re.sub(r"<em>\1</em>", text)
-        else:
-            text = self._strong_re.sub(r"<strong>\2</strong>", text)
-            text = self._em_re.sub(r"<em>\2</em>", text)
-        return text
-
-
-    _block_quote_re = re.compile(r'''
-        (                           # Wrap whole match in \1
-          (
-            ^[ \t]*>[ \t]?          # '>' at the start of a line
-              .+\n                  # rest of the first line
-            (.+\n)*                 # subsequent consecutive lines
-            \n*                     # blanks
-          )+
-        )
-        ''', re.M | re.X)
-    _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M);
-
-    _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S)
-    def _dedent_two_spaces_sub(self, match):
-        return re.sub(r'(?m)^  ', '', match.group(1))
-
-    def _block_quote_sub(self, match):
-        bq = match.group(1)
-        bq = self._bq_one_level_re.sub('', bq)  # trim one level of quoting
-        bq = self._ws_only_line_re.sub('', bq)  # trim whitespace-only lines
-        bq = self._run_block_gamut(bq)          # recurse
-
-        bq = re.sub('(?m)^', '  ', bq)
-        # These leading spaces screw with <pre> content, so we need to fix that:
-        bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq)
-
-        return "<blockquote>\n%s\n</blockquote>\n\n" % bq
-
-    def _do_block_quotes(self, text):
-        if '>' not in text:
-            return text
-        return self._block_quote_re.sub(self._block_quote_sub, text)
-
-    def _form_paragraphs(self, text):
-        # Strip leading and trailing lines:
-        text = text.strip('\n')
-
-        # Wrap <p> tags.
-        grafs = []
-        for i, graf in enumerate(re.split(r"\n{2,}", text)):
-            if graf in self.html_blocks:
-                # Unhashify HTML blocks
-                grafs.append(self.html_blocks[graf])
-            else:
-                cuddled_list = None
-                if "cuddled-lists" in self.extras:
-                    # Need to put back trailing '\n' for `_list_item_re`
-                    # match at the end of the paragraph.
-                    li = self._list_item_re.search(graf + '\n')
-                    # Two of the same list marker in this paragraph: a likely
-                    # candidate for a list cuddled to preceding paragraph
-                    # text (issue 33). Note the `[-1]` is a quick way to
-                    # consider numeric bullets (e.g. "1." and "2.") to be
-                    # equal.
-                    if (li and li.group("next_marker")
-                        and li.group("marker")[-1] == li.group("next_marker")[-1]):
-                        start = li.start()
-                        cuddled_list = self._do_lists(graf[start:]).rstrip("\n")
-                        assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>")
-                        graf = graf[:start]
-
-                # Wrap <p> tags.
-                graf = self._run_span_gamut(graf)
-                grafs.append("<p>" + graf.lstrip(" \t") + "</p>")
-
-                if cuddled_list:
-                    grafs.append(cuddled_list)
-
-        return "\n\n".join(grafs)
-
-    def _add_footnotes(self, text):
-        if self.footnotes:
-            footer = [
-                '<div class="footnotes">',
-                '<hr' + self.empty_element_suffix,
-                '<ol>',
-            ]
-            for i, id in enumerate(self.footnote_ids):
-                if i != 0:
-                    footer.append('')
-                footer.append('<li id="fn-%s">' % id)
-                footer.append(self._run_block_gamut(self.footnotes[id]))
-                backlink = ('<a href="#fnref-%s" '
-                    'class="footnoteBackLink" '
-                    'title="Jump back to footnote %d in the text.">'
-                    '&#8617;</a>' % (id, i+1))
-                if footer[-1].endswith("</p>"):
-                    footer[-1] = footer[-1][:-len("</p>")] \
-                        + '&nbsp;' + backlink + "</p>"
-                else:
-                    footer.append("\n<p>%s</p>" % backlink)
-                footer.append('</li>')
-            footer.append('</ol>')
-            footer.append('</div>')
-            return text + '\n\n' + '\n'.join(footer)
-        else:
-            return text
-
-    # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
-    #   http://bumppo.net/projects/amputator/
-    _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')
-    _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
-    _naked_gt_re = re.compile(r'''(?<![a-z?!/'"-])>''', re.I)
-
-    def _encode_amps_and_angles(self, text):
-        # Smart processing for ampersands and angle brackets that need
-        # to be encoded.
-        text = self._ampersand_re.sub('&amp;', text)
-
-        # Encode naked <'s
-        text = self._naked_lt_re.sub('&lt;', text)
-
-        # Encode naked >'s
-        # Note: Other markdown implementations (e.g. Markdown.pl, PHP
-        # Markdown) don't do this.
-        text = self._naked_gt_re.sub('&gt;', text)
-        return text
-
-    def _encode_backslash_escapes(self, text):
-        for ch, escape in g_escape_table.items():
-            text = text.replace("\\"+ch, escape)
-        return text
-
-    _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I)
-    def _auto_link_sub(self, match):
-        g1 = match.group(1)
-        return '<a href="%s">%s</a>' % (g1, g1)
-
-    _auto_email_link_re = re.compile(r"""
-          <
-           (?:mailto:)?
-          (
-              [-.\w]+
-              \@
-              [-\w]+(\.[-\w]+)*\.[a-z]+
-          )
-          >
-        """, re.I | re.X | re.U)
-    def _auto_email_link_sub(self, match):
-        return self._encode_email_address(
-            self._unescape_special_chars(match.group(1)))
-
-    def _do_auto_links(self, text):
-        text = self._auto_link_re.sub(self._auto_link_sub, text)
-        text = self._auto_email_link_re.sub(self._auto_email_link_sub, text)
-        return text
-
-    def _encode_email_address(self, addr):
-        #  Input: an email address, e.g. "foo@example.com"
-        #
-        #  Output: the email address as a mailto link, with each character
-        #      of the address encoded as either a decimal or hex entity, in
-        #      the hopes of foiling most address harvesting spam bots. E.g.:
-        #
-        #    <a href="&#x6D;&#97;&#105;&#108;&#x74;&#111;:&#102;&#111;&#111;&#64;&#101;
-        #       x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;">&#102;&#111;&#111;
-        #       &#64;&#101;x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;</a>
-        #
-        #  Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
-        #  mailing list: <http://tinyurl.com/yu7ue>
-        chars = [_xml_encode_email_char_at_random(ch)
-                 for ch in "mailto:" + addr]
-        # Strip the mailto: from the visible part.
-        addr = '<a href="%s">%s</a>' \
-               % (''.join(chars), ''.join(chars[7:]))
-        return addr
-
-    def _do_link_patterns(self, text):
-        """Caveat emptor: there isn't much guarding against link
-        patterns being formed inside other standard Markdown links, e.g.
-        inside a [link def][like this].
-
-        Dev Notes: *Could* consider prefixing regexes with a negative
-        lookbehind assertion to attempt to guard against this.
-        """
-        link_from_hash = {}
-        for regex, repl in self.link_patterns:
-            replacements = []
-            for match in regex.finditer(text):
-                if hasattr(repl, "__call__"):
-                    href = repl(match)
-                else:
-                    href = match.expand(repl)
-                replacements.append((match.span(), href))
-            for (start, end), href in reversed(replacements):
-                escaped_href = (
-                    href.replace('"', '&quot;')  # b/c of attr quote
-                        # To avoid markdown <em> and <strong>:
-                        .replace('*', g_escape_table['*'])
-                        .replace('_', g_escape_table['_']))
-                link = '<a href="%s">%s</a>' % (escaped_href, text[start:end])
-                hash = _hash_text(link)
-                link_from_hash[hash] = link
-                text = text[:start] + hash + text[end:]
-        for hash, link in link_from_hash.items():
-            text = text.replace(hash, link)
-        return text
-
-    def _unescape_special_chars(self, text):
-        # Swap back in all the special characters we've hidden.
-        for ch, hash in g_escape_table.items():
-            text = text.replace(hash, ch)
-        return text
-
-    def _outdent(self, text):
-        # Remove one level of line-leading tabs or spaces
-        return self._outdent_re.sub('', text)
+       # The dict of "extras" to enable in processing -- a mapping of
+       # extra name to argument for the extra. Most extras do not have an
+       # argument, in which case the value is None.
+       #
+       # This can be set via (a) subclassing and (b) the constructor
+       # "extras" argument.
+       extras = None
+
+       urls = None
+       titles = None
+       html_blocks = None
+       html_spans = None
+       html_removed_text = "[HTML_REMOVED]"  # for compat with markdown.py
+
+       # Used to track when we're inside an ordered or unordered list
+       # (see _ProcessListItems() for details):
+       list_level = 0
+
+       _ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
+
+       def __init__(self, html4tags=False, tab_width=4, safe_mode=None,
+                                extras=None, link_patterns=None):
+               if html4tags:
+                       self.empty_element_suffix = ">"
+               else:
+                       self.empty_element_suffix = " />"
+               self.tab_width = tab_width
+
+               # For compatibility with earlier markdown2.py and with
+               # markdown.py's safe_mode being a boolean,
+               #       safe_mode == True -> "replace"
+               if safe_mode is True:
+                       self.safe_mode = "replace"
+               else:
+                       self.safe_mode = safe_mode
+
+               if self.extras is None:
+                       self.extras = {}
+               elif not isinstance(self.extras, dict):
+                       self.extras = dict([(e, None) for e in self.extras])
+               if extras:
+                       if not isinstance(extras, dict):
+                               extras = dict([(e, None) for e in extras])
+                       self.extras.update(extras)
+               assert isinstance(self.extras, dict)
+               self._instance_extras = self.extras.copy()
+               self.link_patterns = link_patterns
+               self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
+
+       def reset(self):
+               self.urls = {}
+               self.titles = {}
+               self.html_blocks = {}
+               self.html_spans = {}
+               self.list_level = 0
+               self.extras = self._instance_extras.copy()
+               self.encoding = 'utf-8'
+               if "footnotes" in self.extras:
+                       self.footnotes = {}
+                       self.footnote_ids = []
+
+       def convert(self, text, encoding=None):
+               """Convert the given text."""
+               # Main function. The order in which other subs are called here is
+               # essential. Link and image substitutions need to happen before
+               # _EscapeSpecialChars(), so that any *'s or _'s in the <a>
+               # and <img> tags get encoded.
+
+               # Clear the global hashes. If we don't clear these, you get conflicts
+               # from other articles when generating a page which contains more than
+               # one article (e.g. an index page that shows the N most recent
+               # articles):
+               self.reset()
+               if encoding:
+                       self.encoding = encoding
+
+               if not isinstance(text, unicode):
+                       text = unicode(text, self.encoding)
+
+               # Standardize line endings:
+               #text = re.sub("\r\n|\r", "\n", text)
+
+               # Make sure $text ends with a couple of newlines:
+               text += "\n\n"
+
+               # Convert all tabs to spaces.
+               text = self._detab(text)
+
+               # Strip any lines consisting only of spaces and tabs.
+               # This makes subsequent regexen easier to write, because we can
+               # match consecutive blank lines with /\n+/ instead of something
+               # contorted like /[ \t]*\n+/ .
+               text = self._ws_only_line_re.sub("", text)
+
+               if self.safe_mode:
+                       text = self._hash_html_spans(text)
+
+               # Turn block-level HTML blocks into hash entries
+               text = self._hash_html_blocks(text, raw=True)
+
+               # Strip link definitions, store in hashes.
+               if "footnotes" in self.extras:
+                       # Must do footnotes first because an unlucky footnote defn
+                       # looks like a link defn:
+                       #       [^4]: this "looks like a link defn"
+                       text = self._strip_footnote_definitions(text)
+               text = self._strip_link_definitions(text)
+
+               text = self._run_block_gamut(text)
+
+               if "footnotes" in self.extras:
+                       text = self._add_footnotes(text)
+
+               text = self._unescape_special_chars(text)
+
+               if self.safe_mode:
+                       text = self._unhash_html_spans(text)
+
+               text += "\n"
+               return text
+
+       # Cribbed from a post by Bart Lateur:
+       # <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>
+       _detab_re = re.compile(r'(.*?)\t', re.M)
+       def _detab_sub(self, match):
+               g1 = match.group(1)
+               return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width))
+       def _detab(self, text):
+               r"""Remove (leading?) tabs from a file.
+
+                       >>> m = Markdown()
+                       >>> m._detab("\tfoo")
+                       '        foo'
+                       >>> m._detab("  \tfoo")
+                       '        foo'
+                       >>> m._detab("\t  foo")
+                       '          foo'
+                       >>> m._detab("  foo")
+                       '  foo'
+                       >>> m._detab("  foo\n\tbar\tblam")
+                       '  foo\n        bar blam'
+               """
+               if '\t' not in text:
+                       return text
+               return self._detab_re.subn(self._detab_sub, text)[0]
+
+       _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del'
+       _strict_tag_block_re = re.compile(r"""
+               (                                               # save in \1
+                       ^                                       # start of line  (with re.M)
+                       <(%s)                           # start tag = \2
+                       \b                                      # word break
+                       (.*\n)*?                        # any number of lines, minimally matching
+                       </\2>                           # the matching end tag
+                       [ \t]*                          # trailing spaces/tabs
+                       (?=\n+|\Z)                      # followed by a newline or end of document
+               )
+               """ % _block_tags_a,
+               re.X | re.M)
+
+       _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math'
+       _liberal_tag_block_re = re.compile(r"""
+               (                                               # save in \1
+                       ^                                       # start of line  (with re.M)
+                       <(%s)                           # start tag = \2
+                       \b                                      # word break
+                       (.*\n)*?                        # any number of lines, minimally matching
+                       .*</\2>                         # the matching end tag
+                       [ \t]*                          # trailing spaces/tabs
+                       (?=\n+|\Z)                      # followed by a newline or end of document
+               )
+               """ % _block_tags_b,
+               re.X | re.M)
+
+       def _hash_html_block_sub(self, match, raw=False):
+               html = match.group(1)
+               if raw and self.safe_mode:
+                       html = self._sanitize_html(html)
+               key = _hash_text(html)
+               self.html_blocks[key] = html
+               return "\n\n" + key + "\n\n"
+
+       def _hash_html_blocks(self, text, raw=False):
+               """Hashify HTML blocks
+
+               We only want to do this for block-level HTML tags, such as headers,
+               lists, and tables. That's because we still want to wrap <p>s around
+               "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+               phrase emphasis, and spans. The list of tags we're looking for is
+               hard-coded.
+
+               @param raw {boolean} indicates if these are raw HTML blocks in
+                       the original source. It makes a difference in "safe" mode.
+               """
+               if '<' not in text:
+                       return text
+
+               # Pass `raw` value into our calls to self._hash_html_block_sub.
+               hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw)
+
+               # First, look for nested blocks, e.g.:
+               #       <div>
+               #               <div>
+               #               tags for inner block must be indented.
+               #               </div>
+               #       </div>
+               #
+               # The outermost tags must start at the left margin for this to match, and
+               # the inner nested divs must be indented.
+               # We need to do this before the next, more liberal match, because the next
+               # match will start at the first `<div>` and stop at the first `</div>`.
+               text = self._strict_tag_block_re.sub(hash_html_block_sub, text)
+
+               # Now match more liberally, simply from `\n<tag>` to `</tag>\n`
+               text = self._liberal_tag_block_re.sub(hash_html_block_sub, text)
+
+               # Special case just for <hr />. It was easier to make a special
+               # case than to make the other regex more complicated.
+               if "<hr" in text:
+                       _hr_tag_re = _hr_tag_re_from_tab_width(self.tab_width)
+                       text = _hr_tag_re.sub(hash_html_block_sub, text)
+
+               # Special case for standalone HTML comments:
+               if "<!--" in text:
+                       start = 0
+                       while True:
+                               # Delimiters for next comment block.
+                               try:
+                                       start_idx = text.index("<!--", start)
+                               except ValueError, ex:
+                                       break
+                               try:
+                                       end_idx = text.index("-->", start_idx) + 3
+                               except ValueError, ex:
+                                       break
+
+                               # Start position for next comment block search.
+                               start = end_idx
+
+                               # Validate whitespace before comment.
+                               if start_idx:
+                                       # - Up to `tab_width - 1` spaces before start_idx.
+                                       for i in range(self.tab_width - 1):
+                                               if text[start_idx - 1] != ' ':
+                                                       break
+                                               start_idx -= 1
+                                               if start_idx == 0:
+                                                       break
+                                       # - Must be preceded by 2 newlines or hit the start of
+                                       #       the document.
+                                       if start_idx == 0:
+                                               pass
+                                       elif start_idx == 1 and text[0] == '\n':
+                                               start_idx = 0  # to match minute detail of Markdown.pl regex
+                                       elif text[start_idx-2:start_idx] == '\n\n':
+                                               pass
+                                       else:
+                                               break
+
+                               # Validate whitespace after comment.
+                               # - Any number of spaces and tabs.
+                               while end_idx < len(text):
+                                       if text[end_idx] not in ' \t':
+                                               break
+                                       end_idx += 1
+                               # - Must be following by 2 newlines or hit end of text.
+                               if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'):
+                                       continue
+
+                               # Escape and hash (must match `_hash_html_block_sub`).
+                               html = text[start_idx:end_idx]
+                               if raw and self.safe_mode:
+                                       html = self._sanitize_html(html)
+                               key = _hash_text(html)
+                               self.html_blocks[key] = html
+                               text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:]
+
+               if "xml" in self.extras:
+                       # Treat XML processing instructions and namespaced one-liner
+                       # tags as if they were block HTML tags. E.g., if standalone
+                       # (i.e. are their own paragraph), the following do not get
+                       # wrapped in a <p> tag:
+                       #        <?foo bar?>
+                       #
+                       #        <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="chapter_1.md"/>
+                       _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width)
+                       text = _xml_oneliner_re.sub(hash_html_block_sub, text)
+
+               return text
+
+       def _strip_link_definitions(self, text):
+               # Strips link definitions from text, stores the URLs and titles in
+               # hash references.
+               less_than_tab = self.tab_width - 1
+
+               # Link defs are in the form:
+               #       [id]: url "optional title"
+               _link_def_re = re.compile(r"""
+                       ^[ ]{0,%d}\[(.+)\]: # id = \1
+                         [ \t]*
+                         \n?                           # maybe *one* newline
+                         [ \t]*
+                       <?(.+?)>?                       # url = \2
+                         [ \t]*
+                       (?:
+                               \n?                             # maybe one newline
+                               [ \t]*
+                               (?<=\s)                 # lookbehind for whitespace
+                               ['"(]
+                               ([^\n]*)                # title = \3
+                               ['")]
+                               [ \t]*
+                       )?      # title is optional
+                       (?:\n+|\Z)
+                       """ % less_than_tab, re.X | re.M | re.U)
+               return _link_def_re.sub(self._extract_link_def_sub, text)
+
+       def _extract_link_def_sub(self, match):
+               id, url, title = match.groups()
+               key = id.lower()        # Link IDs are case-insensitive
+               self.urls[key] = self._encode_amps_and_angles(url)
+               if title:
+                       self.titles[key] = title.replace('"', '&quot;')
+               return ""
+
+       def _extract_footnote_def_sub(self, match):
+               id, text = match.groups()
+               text = _dedent(text, skip_first_line=not text.startswith('\n')).strip()
+               normed_id = re.sub(r'\W', '-', id)
+               # Ensure footnote text ends with a couple newlines (for some
+               # block gamut matches).
+               self.footnotes[normed_id] = text + "\n\n"
+               return ""
+
+       def _strip_footnote_definitions(self, text):
+               """A footnote definition looks like this:
+
+                       [^note-id]: Text of the note.
+
+                               May include one or more indented paragraphs.
+
+               Where,
+               - The 'note-id' can be pretty much anything, though typically it
+                 is the number of the footnote.
+               - The first paragraph may start on the next line, like so:
+
+                       [^note-id]:
+                               Text of the note.
+               """
+               less_than_tab = self.tab_width - 1
+               footnote_def_re = re.compile(r'''
+                       ^[ ]{0,%d}\[\^(.+)\]:   # id = \1
+                       [ \t]*
+                       (                                               # footnote text = \2
+                         # First line need not start with the spaces.
+                         (?:\s*.*\n+)
+                         (?:
+                               (?:[ ]{%d} | \t)  # Subsequent lines must be indented.
+                               .*\n+
+                         )*
+                       )
+                       # Lookahead for non-space at line-start, or end of doc.
+                       (?:(?=^[ ]{0,%d}\S)|\Z)
+                       ''' % (less_than_tab, self.tab_width, self.tab_width),
+                       re.X | re.M)
+               return footnote_def_re.sub(self._extract_footnote_def_sub, text)
+
+
+       _hr_res = [
+               re.compile(r"^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$", re.M),
+               re.compile(r"^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$", re.M),
+               re.compile(r"^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$", re.M),
+       ]
+
+       def _run_block_gamut(self, text):
+               # These are all the transformations that form block-level
+               # tags like paragraphs, headers, and list items.
+
+               text = self._do_headers(text)
+
+               # Do Horizontal Rules:
+               hr = "\n<hr"+self.empty_element_suffix+"\n"
+               for hr_re in self._hr_res:
+                       text = hr_re.sub(hr, text)
+
+               text = self._do_lists(text)
+
+               if "pyshell" in self.extras:
+                       text = self._prepare_pyshell_blocks(text)
+
+               text = self._do_code_blocks(text)
+
+               text = self._do_block_quotes(text)
+
+               # We already ran _HashHTMLBlocks() before, in Markdown(), but that
+               # was to escape raw HTML in the original Markdown source. This time,
+               # we're escaping the markup we've just created, so that we don't wrap
+               # <p> tags around block-level tags.
+               text = self._hash_html_blocks(text)
+
+               text = self._form_paragraphs(text)
+
+               return text
+
+       def _pyshell_block_sub(self, match):
+               lines = match.group(0).splitlines(0)
+               _dedentlines(lines)
+               indent = ' ' * self.tab_width
+               s = ('\n' # separate from possible cuddled paragraph
+                        + indent + ('\n'+indent).join(lines)
+                        + '\n\n')
+               return s
+
+       def _prepare_pyshell_blocks(self, text):
+               """Ensure that Python interactive shell sessions are put in
+               code blocks -- even if not properly indented.
+               """
+               if ">>>" not in text:
+                       return text
+
+               less_than_tab = self.tab_width - 1
+               _pyshell_block_re = re.compile(r"""
+                       ^([ ]{0,%d})>>>[ ].*\n   # first line
+                       ^(\1.*\S+.*\n)*                 # any number of subsequent lines
+                       ^\n                                             # ends with a blank line
+                       """ % less_than_tab, re.M | re.X)
+
+               return _pyshell_block_re.sub(self._pyshell_block_sub, text)
+
+       def _run_span_gamut(self, text):
+               # These are all the transformations that occur *within* block-level
+               # tags like paragraphs, headers, and list items.
+
+               text = self._do_code_spans(text)
+
+               text = self._escape_special_chars(text)
+
+               # Process anchor and image tags.
+               #text = self._do_links(text)
+
+               # Make links out of things like `<http://example.com/>`
+               # Must come after _do_links(), because you can use < and >
+               # delimiters in inline links like [this](<url>).
+               text = self._do_auto_links(text)
+
+               if "link-patterns" in self.extras:
+                       text = self._do_link_patterns(text)
+
+               text = self._encode_amps_and_angles(text)
+
+               text = self._do_italics_and_bold(text)
+
+               # Do hard breaks:
+               text = re.sub(r" {2,}\n", " <br%s\n" % self.empty_element_suffix, text)
+
+               return text
+
+       # "Sorta" because auto-links are identified as "tag" tokens.
+       _sorta_html_tokenize_re = re.compile(r"""
+               (
+                       # tag
+                       </?
+                       (?:\w+)                                                                         # tag name
+                       (?:\s+(?:[\w-]+:)?[\w-]+=(?:".*?"|'.*?'))*      # attributes
+                       \s*/?>
+                       |
+                       # auto-link (e.g., <http://www.activestate.com/>)
+                       <\w+[^>]*>
+                       |
+                       <!--.*?-->              # comment
+                       |
+                       <\?.*?\?>               # processing instruction
+               )
+               """, re.X)
+
+       def _escape_special_chars(self, text):
+               # Python markdown note: the HTML tokenization here differs from
+               # that in Markdown.pl, hence the behaviour for subtle cases can
+               # differ (I believe the tokenizer here does a better job because
+               # it isn't susceptible to unmatched '<' and '>' in HTML tags).
+               # Note, however, that '>' is not allowed in an auto-link URL
+               # here.
+               escaped = []
+               is_html_markup = False
+               for token in self._sorta_html_tokenize_re.split(text):
+                       if is_html_markup:
+                               # Within tags/HTML-comments/auto-links, encode * and _
+                               # so they don't conflict with their use in Markdown for
+                               # italics and strong.  We're replacing each such
+                               # character with its corresponding MD5 checksum value;
+                               # this is likely overkill, but it should prevent us from
+                               # colliding with the escape values by accident.
+                               escaped.append(token.replace('*', g_escape_table['*'])
+                                                                       .replace('_', g_escape_table['_']))
+                       else:
+                               escaped.append(self._encode_backslash_escapes(token))
+                       is_html_markup = not is_html_markup
+               return ''.join(escaped)
+
+       def _hash_html_spans(self, text):
+               # Used for safe_mode.
+
+               def _is_auto_link(s):
+                       if ':' in s and self._auto_link_re.match(s):
+                               return True
+                       elif '@' in s and self._auto_email_link_re.match(s):
+                               return True
+                       return False
+
+               tokens = []
+               is_html_markup = False
+               for token in self._sorta_html_tokenize_re.split(text):
+                       if is_html_markup and not _is_auto_link(token):
+                               sanitized = self._sanitize_html(token)
+                               key = _hash_text(sanitized)
+                               self.html_spans[key] = sanitized
+                               tokens.append(key)
+                       else:
+                               tokens.append(token)
+                       is_html_markup = not is_html_markup
+               return ''.join(tokens)
+
+       def _unhash_html_spans(self, text):
+               for key, sanitized in self.html_spans.items():
+                       text = text.replace(key, sanitized)
+               return text
+
+       def _sanitize_html(self, s):
+               if self.safe_mode == "replace":
+                       return self.html_removed_text
+               elif self.safe_mode == "escape":
+                       replacements = [
+                               ('&', '&amp;'),
+                               ('<', '&lt;'),
+                               ('>', '&gt;'),
+                       ]
+                       for before, after in replacements:
+                               s = s.replace(before, after)
+                       return s
+               else:
+                       raise MarkdownError("invalid value for 'safe_mode': %r (must be "
+                                                               "'escape' or 'replace')" % self.safe_mode)
+
+       _tail_of_inline_link_re = re.compile(r'''
+                 # Match tail of: [text](/url/) or [text](/url/ "title")
+                 \(                    # literal paren
+                       [ \t]*
+                       (?P<url>                        # \1
+                               <.*?>
+                               |
+                               .*?
+                       )
+                       [ \t]*
+                       (                                       # \2
+                         (['"])                        # quote char = \3
+                         (?P<title>.*?)
+                         \3                            # matching quote
+                       )?                                      # title is optional
+                 \)
+               ''', re.X | re.S)
+       _tail_of_reference_link_re = re.compile(r'''
+                 # Match tail of: [text][id]
+                 [ ]?                  # one optional space
+                 (?:\n[ ]*)?   # one optional newline followed by spaces
+                 \[
+                       (?P<id>.*?)
+                 \]
+               ''', re.X | re.S)
+
+       def _do_links(self, text):
+               """Turn Markdown link shortcuts into XHTML <a> and <img> tags.
+
+               This is a combination of Markdown.pl's _DoAnchors() and
+               _DoImages(). They are done together because that simplified the
+               approach. It was necessary to use a different approach than
+               Markdown.pl because of the lack of atomic matching support in
+               Python's regex engine used in $g_nested_brackets.
+               """
+               MAX_LINK_TEXT_SENTINEL = 3000  # markdown2 issue 24
+
+               # `anchor_allowed_pos` is used to support img links inside
+               # anchors, but not anchors inside anchors. An anchor's start
+               # pos must be `>= anchor_allowed_pos`.
+               anchor_allowed_pos = 0
+
+               curr_pos = 0
+               while True: # Handle the next link.
+                       # The next '[' is the start of:
+                       # - an inline anchor:   [text](url "title")
+                       # - a reference anchor: [text][id]
+                       # - an inline img:              ![text](url "title")
+                       # - a reference img:    ![text][id]
+                       # - a footnote ref:             [^id]
+                       #       (Only if 'footnotes' extra enabled)
+                       # - a footnote defn:    [^id]: ...
+                       #       (Only if 'footnotes' extra enabled) These have already
+                       #       been stripped in _strip_footnote_definitions() so no
+                       #       need to watch for them.
+                       # - a link definition:  [id]: url "title"
+                       #       These have already been stripped in
+                       #       _strip_link_definitions() so no need to watch for them.
+                       # - not markup:                 [...anything else...
+                       try:
+                               start_idx = text.index('[', curr_pos)
+                       except ValueError:
+                               break
+                       text_length = len(text)
+
+                       # Find the matching closing ']'.
+                       # Markdown.pl allows *matching* brackets in link text so we
+                       # will here too. Markdown.pl *doesn't* currently allow
+                       # matching brackets in img alt text -- we'll differ in that
+                       # regard.
+                       bracket_depth = 0
+                       for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL,
+                                                                                       text_length)):
+                               ch = text[p]
+                               if ch == ']':
+                                       bracket_depth -= 1
+                                       if bracket_depth < 0:
+                                               break
+                               elif ch == '[':
+                                       bracket_depth += 1
+                       else:
+                               # Closing bracket not found within sentinel length.
+                               # This isn't markup.
+                               curr_pos = start_idx + 1
+                               continue
+                       link_text = text[start_idx+1:p]
+
+                       # Possibly a footnote ref?
+                       if "footnotes" in self.extras and link_text.startswith("^"):
+                               normed_id = re.sub(r'\W', '-', link_text[1:])
+                               if normed_id in self.footnotes:
+                                       self.footnote_ids.append(normed_id)
+                                       result = '<sup class="footnote-ref" id="fnref-%s">' \
+                                                        '<a href="#fn-%s">%s</a></sup>' \
+                                                        % (normed_id, normed_id, len(self.footnote_ids))
+                                       text = text[:start_idx] + result + text[p+1:]
+                               else:
+                                       # This id isn't defined, leave the markup alone.
+                                       curr_pos = p+1
+                               continue
+
+                       # Now determine what this is by the remainder.
+                       p += 1
+                       if p == text_length:
+                               return text
+
+                       # Inline anchor or img?
+                       if text[p] == '(': # attempt at perf improvement
+                               match = self._tail_of_inline_link_re.match(text, p)
+                               if match:
+                                       # Handle an inline anchor or img.
+                                       is_img = start_idx > 0 and text[start_idx-1] == "!"
+                                       if is_img:
+                                               start_idx -= 1
+
+                                       url, title = match.group("url"), match.group("title")
+                                       if url and url[0] == '<':
+                                               url = url[1:-1]  # '<url>' -> 'url'
+                                       # We've got to encode these to avoid conflicting
+                                       # with italics/bold.
+                                       url = url.replace('*', g_escape_table['*']) \
+                                                        .replace('_', g_escape_table['_'])
+                                       if title:
+                                               title_str = ' title="%s"' \
+                                                       % title.replace('*', g_escape_table['*']) \
+                                                                  .replace('_', g_escape_table['_']) \
+                                                                  .replace('"', '&quot;')
+                                       else:
+                                               title_str = ''
+                                       if is_img:
+                                               result = '<img src="%s" alt="%s"%s%s' \
+                                                       % (url.replace('"', '&quot;'),
+                                                          link_text.replace('"', '&quot;'),
+                                                          title_str, self.empty_element_suffix)
+                                               curr_pos = start_idx + len(result)
+                                               text = text[:start_idx] + result + text[match.end():]
+                                       elif start_idx >= anchor_allowed_pos:
+                                               result_head = '<a href="%s"%s>' % (url, title_str)
+                                               result = '%s%s</a>' % (result_head, link_text)
+                                               # <img> allowed from curr_pos on, <a> from
+                                               # anchor_allowed_pos on.
+                                               curr_pos = start_idx + len(result_head)
+                                               anchor_allowed_pos = start_idx + len(result)
+                                               text = text[:start_idx] + result + text[match.end():]
+                                       else:
+                                               # Anchor not allowed here.
+                                               curr_pos = start_idx + 1
+                                       continue
+
+                       # Reference anchor or img?
+                       else:
+                               match = self._tail_of_reference_link_re.match(text, p)
+                               if match:
+                                       # Handle a reference-style anchor or img.
+                                       is_img = start_idx > 0 and text[start_idx-1] == "!"
+                                       if is_img:
+                                               start_idx -= 1
+                                       link_id = match.group("id").lower()
+                                       if not link_id:
+                                               link_id = link_text.lower()      # for links like [this][]
+                                       if link_id in self.urls:
+                                               url = self.urls[link_id]
+                                               # We've got to encode these to avoid conflicting
+                                               # with italics/bold.
+                                               url = url.replace('*', g_escape_table['*']) \
+                                                                .replace('_', g_escape_table['_'])
+                                               title = self.titles.get(link_id)
+                                               if title:
+                                                       title = title.replace('*', g_escape_table['*']) \
+                                                                                .replace('_', g_escape_table['_'])
+                                                       title_str = ' title="%s"' % title
+                                               else:
+                                                       title_str = ''
+                                               if is_img:
+                                                       result = '<img src="%s" alt="%s"%s%s' \
+                                                               % (url.replace('"', '&quot;'),
+                                                                  link_text.replace('"', '&quot;'),
+                                                                  title_str, self.empty_element_suffix)
+                                                       curr_pos = start_idx + len(result)
+                                                       text = text[:start_idx] + result + text[match.end():]
+                                               elif start_idx >= anchor_allowed_pos:
+                                                       result = '<a href="%s"%s>%s</a>' \
+                                                               % (url, title_str, link_text)
+                                                       result_head = '<a href="%s"%s>' % (url, title_str)
+                                                       result = '%s%s</a>' % (result_head, link_text)
+                                                       # <img> allowed from curr_pos on, <a> from
+                                                       # anchor_allowed_pos on.
+                                                       curr_pos = start_idx + len(result_head)
+                                                       anchor_allowed_pos = start_idx + len(result)
+                                                       text = text[:start_idx] + result + text[match.end():]
+                                               else:
+                                                       # Anchor not allowed here.
+                                                       curr_pos = start_idx + 1
+                                       else:
+                                               # This id isn't defined, leave the markup alone.
+                                               curr_pos = match.end()
+                                       continue
+
+                       # Otherwise, it isn't markup.
+                       curr_pos = start_idx + 1
+
+               return text
+
+
+       _setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M)
+       def _setext_h_sub(self, match):
+               n = {"=": 1, "-": 2}[match.group(2)[0]]
+               demote_headers = self.extras.get("demote-headers")
+               if demote_headers:
+                       n = min(n + demote_headers, 6)
+               return "<h%d>%s</h%d>\n\n" \
+                          % (n, self._run_span_gamut(match.group(1)), n)
+
+       _atx_h_re = re.compile(r'''
+               ^([\#=]{1,6})  # \1 = string of #'s
+               [ \t]*
+               (.+?)           # \2 = Header text
+               [ \t]*
+               (?<!\\)         # ensure not an escaped trailing '#'
+               [\#=]*          # optional closing #'s (not counted)
+               \n+
+               ''', re.X | re.M)
+       def _atx_h_sub(self, match):
+               n = len(match.group(1))
+               demote_headers = self.extras.get("demote-headers")
+               if demote_headers:
+                       n = min(n + demote_headers, 6)
+               return "<h%d>%s</h%d>\n\n" \
+                          % (n, self._run_span_gamut(match.group(2)), n)
+
+       def _do_headers(self, text):
+               # Setext-style headers:
+               #         Header 1
+               #         ========
+               #
+               #         Header 2
+               #         --------
+               text = self._setext_h_re.sub(self._setext_h_sub, text)
+
+               # atx-style headers:
+               #       # Header 1
+               #       ## Header 2
+               #       ## Header 2 with closing hashes ##
+               #       ...
+               #       ###### Header 6
+               text = self._atx_h_re.sub(self._atx_h_sub, text)
+
+               return text
+
+
+       _marker_ul_chars  = '*+-'
+       _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars
+       _marker_ul = '(?:[%s])' % _marker_ul_chars
+       _marker_ol = r'(?:\d+\.)'
+
+       def _list_sub(self, match):
+               lst = match.group(1)
+               lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol"
+               result = self._process_list_items(lst)
+               if self.list_level:
+                       return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type)
+               else:
+                       return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type)
+
+       def _do_lists(self, text):
+               # Form HTML ordered (numbered) and unordered (bulleted) lists.
+
+               for marker_pat in (self._marker_ul, self._marker_ol):
+                       # Re-usable pattern to match any entire ul or ol list:
+                       less_than_tab = self.tab_width - 1
+                       whole_list = r'''
+                               (                                       # \1 = whole list
+                                 (                                     # \2
+                                       [ ]{0,%d}
+                                       (%s)                    # \3 = first list item marker
+                                       [ \t]+
+                                 )
+                                 (?:.+?)
+                                 (                                     # \4
+                                         \Z
+                                       |
+                                         \n{2,}
+                                         (?=\S)
+                                         (?!                   # Negative lookahead for another list item marker
+                                               [ \t]*
+                                               %s[ \t]+
+                                         )
+                                 )
+                               )
+                       ''' % (less_than_tab, marker_pat, marker_pat)
+
+                       # We use a different prefix before nested lists than top-level lists.
+                       # See extended comment in _process_list_items().
+                       #
+                       # Note: There's a bit of duplication here. My original implementation
+                       # created a scalar regex pattern as the conditional result of the test on
+                       # $g_list_level, and then only ran the $text =~ s{...}{...}egmx
+                       # substitution once, using the scalar as the pattern. This worked,
+                       # everywhere except when running under MT on my hosting account at Pair
+                       # Networks. There, this caused all rebuilds to be killed by the reaper (or
+                       # perhaps they crashed, but that seems incredibly unlikely given that the
+                       # same script on the same server ran fine *except* under MT. I've spent
+                       # more time trying to figure out why this is happening than I'd like to
+                       # admit. My only guess, backed up by the fact that this workaround works,
+                       # is that Perl optimizes the substition when it can figure out that the
+                       # pattern will never change, and when this optimization isn't on, we run
+                       # afoul of the reaper. Thus, the slightly redundant code to that uses two
+                       # static s/// patterns rather than one conditional pattern.
+
+                       if self.list_level:
+                               sub_list_re = re.compile("^"+whole_list, re.X | re.M | re.S)
+                               text = sub_list_re.sub(self._list_sub, text)
+                       else:
+                               list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list,
+                                                                        re.X | re.M | re.S)
+                               text = list_re.sub(self._list_sub, text)
+
+               return text
+
+       _list_item_re = re.compile(r'''
+               (\n)?                           # leading line = \1
+               (^[ \t]*)                       # leading whitespace = \2
+               (?P<marker>%s) [ \t]+   # list marker = \3
+               ((?:.+?)                        # list item text = \4
+                (\n{1,2}))                     # eols = \5
+               (?= \n* (\Z | \2 (?P<next_marker>%s) [ \t]+))
+               ''' % (_marker_any, _marker_any),
+               re.M | re.X | re.S)
+
+       _last_li_endswith_two_eols = False
+       def _list_item_sub(self, match):
+               item = match.group(4)
+               leading_line = match.group(1)
+               leading_space = match.group(2)
+               if leading_line or "\n\n" in item or self._last_li_endswith_two_eols:
+                       item = self._run_block_gamut(self._outdent(item))
+               else:
+                       # Recursion for sub-lists:
+                       item = self._do_lists(self._outdent(item))
+                       if item.endswith('\n'):
+                               item = item[:-1]
+                       item = self._run_span_gamut(item)
+               self._last_li_endswith_two_eols = (len(match.group(5)) == 2)
+               return "<li>%s</li>\n" % item
+
+       def _process_list_items(self, list_str):
+               # Process the contents of a single ordered or unordered list,
+               # splitting it into individual list items.
+
+               # The $g_list_level global keeps track of when we're inside a list.
+               # Each time we enter a list, we increment it; when we leave a list,
+               # we decrement. If it's zero, we're not in a list anymore.
+               #
+               # We do this because when we're not inside a list, we want to treat
+               # something like this:
+               #
+               #               I recommend upgrading to version
+               #               8. Oops, now this line is treated
+               #               as a sub-list.
+               #
+               # As a single paragraph, despite the fact that the second line starts
+               # with a digit-period-space sequence.
+               #
+               # Whereas when we're inside a list (or sub-list), that line will be
+               # treated as the start of a sub-list. What a kludge, huh? This is
+               # an aspect of Markdown's syntax that's hard to parse perfectly
+               # without resorting to mind-reading. Perhaps the solution is to
+               # change the syntax rules such that sub-lists must start with a
+               # starting cardinal number; e.g. "1." or "a.".
+               self.list_level += 1
+               self._last_li_endswith_two_eols = False
+               list_str = list_str.rstrip('\n') + '\n'
+               list_str = self._list_item_re.sub(self._list_item_sub, list_str)
+               self.list_level -= 1
+               return list_str
+
+       def _get_pygments_lexer(self, lexer_name):
+               try:
+                       from pygments import lexers, util
+               except ImportError:
+                       return None
+               try:
+                       return lexers.get_lexer_by_name(lexer_name)
+               except util.ClassNotFound:
+                       return None
+
+       def _color_with_pygments(self, codeblock, lexer, **formatter_opts):
+               import pygments
+               import pygments.formatters
+
+               class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
+                       def _wrap_code(self, inner):
+                               """A function for use in a Pygments Formatter which
+                               wraps in <code> tags.
+                               """
+                               yield 0, "<code>"
+                               for tup in inner:
+                                       yield tup
+                               yield 0, "</code>"
+
+                       def wrap(self, source, outfile):
+                               """Return the source with a code, pre, and div."""
+                               return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
+
+               formatter = HtmlCodeFormatter(cssclass="codehilite", **formatter_opts)
+               return pygments.highlight(codeblock, lexer, formatter)
+
+       def _code_block_sub(self, match):
+               codeblock = match.group(1)
+               codeblock = self._outdent(codeblock)
+               codeblock = self._detab(codeblock)
+               codeblock = codeblock.lstrip('\n')      # trim leading newlines
+               codeblock = codeblock.rstrip()          # trim trailing whitespace
+
+               if "code-color" in self.extras and codeblock.startswith(":::"):
+                       lexer_name, rest = codeblock.split('\n', 1)
+                       lexer_name = lexer_name[3:].strip()
+                       lexer = self._get_pygments_lexer(lexer_name)
+                       codeblock = rest.lstrip("\n")   # Remove lexer declaration line.
+                       if lexer:
+                               formatter_opts = self.extras['code-color'] or {}
+                               colored = self._color_with_pygments(codeblock, lexer,
+                                                                                                       **formatter_opts)
+                               return "\n\n%s\n\n" % colored
+
+               codeblock = self._encode_code(codeblock)
+               return "\n\n<pre><code>%s\n</code></pre>\n\n" % codeblock
+
+       def _do_code_blocks(self, text):
+               """Process Markdown `<pre><code>` blocks."""
+               code_block_re = re.compile(r'''
+                       (?:\n\n|\A)
+                       (                               # $1 = the code block -- one or more lines, starting with a space/tab
+                         (?:
+                               (?:[ ]{%d} | \t)  # Lines must start with a tab or a tab-width of spaces
+                               .*\n+
+                         )+
+                       )
+                       ((?=^[ ]{0,%d}\S)|\Z)   # Lookahead for non-space at line-start, or end of doc
+                       ''' % (self.tab_width, self.tab_width),
+                       re.M | re.X)
+
+               return code_block_re.sub(self._code_block_sub, text)
+
+
+       # Rules for a code span:
+       # - backslash escapes are not interpreted in a code span
+       # - to include one or or a run of more backticks the delimiters must
+       #       be a longer run of backticks
+       # - cannot start or end a code span with a backtick; pad with a
+       #       space and that space will be removed in the emitted HTML
+       # See `test/tm-cases/escapes.text` for a number of edge-case
+       # examples.
+       _code_span_re = re.compile(r'''
+                       (?<!\\)
+                       (`+)            # \1 = Opening run of `
+                       (?!`)           # See Note A test/tm-cases/escapes.text
+                       (.+?)           # \2 = The code block
+                       (?<!`)
+                       \1                      # Matching closer
+                       (?!`)
+               ''', re.X | re.S)
+
+       def _code_span_sub(self, match):
+               c = match.group(2).strip(" \t")
+               c = self._encode_code(c)
+               return "<code>%s</code>" % c
+
+       def _do_code_spans(self, text):
+               #       *       Backtick quotes are used for <code></code> spans.
+               #
+               #       *       You can use multiple backticks as the delimiters if you want to
+               #               include literal backticks in the code span. So, this input:
+               #
+               #                 Just type ``foo `bar` baz`` at the prompt.
+               #
+               #               Will translate to:
+               #
+               #                 <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
+               #
+               #               There's no arbitrary limit to the number of backticks you
+               #               can use as delimters. If you need three consecutive backticks
+               #               in your code, use four for delimiters, etc.
+               #
+               #       *       You can use spaces to get literal backticks at the edges:
+               #
+               #                 ... type `` `bar` `` ...
+               #
+               #               Turns to:
+               #
+               #                 ... type <code>`bar`</code> ...
+               return self._code_span_re.sub(self._code_span_sub, text)
+
+       def _encode_code(self, text):
+               """Encode/escape certain characters inside Markdown code runs.
+               The point is that in code, these characters are literals,
+               and lose their special Markdown meanings.
+               """
+               replacements = [
+                       # Encode all ampersands; HTML entities are not
+                       # entities within a Markdown code span.
+                       ('&', '&amp;'),
+                       # Do the angle bracket song and dance:
+                       ('<', '&lt;'),
+                       ('>', '&gt;'),
+                       # Now, escape characters that are magic in Markdown:
+                       ('*', g_escape_table['*']),
+                       ('_', g_escape_table['_']),
+                       ('{', g_escape_table['{']),
+                       ('}', g_escape_table['}']),
+                       ('[', g_escape_table['[']),
+                       (']', g_escape_table[']']),
+                       ('\\', g_escape_table['\\']),
+               ]
+               for before, after in replacements:
+                       text = text.replace(before, after)
+               return text
+
+       _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S)
+       _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S)
+       _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S)
+       _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S)
+       def _do_italics_and_bold(self, text):
+               # <strong> must go first:
+               if "code-friendly" in self.extras:
+                       text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text)
+                       text = self._code_friendly_em_re.sub(r"<em>\1</em>", text)
+               else:
+                       text = self._strong_re.sub(r"<strong>\2</strong>", text)
+                       text = self._em_re.sub(r"<em>\2</em>", text)
+               return text
+
+
+       _block_quote_re = re.compile(r'''
+               (                                                       # Wrap whole match in \1
+                 (
+                       ^[ \t]*>[ \t]?                  # '>' at the start of a line
+                         .+\n                                  # rest of the first line
+                       (.+\n)*                                 # subsequent consecutive lines
+                       \n*                                             # blanks
+                 )+
+               )
+               ''', re.M | re.X)
+       _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M);
+
+       _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S)
+       def _dedent_two_spaces_sub(self, match):
+               return re.sub(r'(?m)^  ', '', match.group(1))
+
+       def _block_quote_sub(self, match):
+               bq = match.group(1)
+               bq = self._bq_one_level_re.sub('', bq)  # trim one level of quoting
+               bq = self._ws_only_line_re.sub('', bq)  # trim whitespace-only lines
+               bq = self._run_block_gamut(bq)                  # recurse
+
+               bq = re.sub('(?m)^', '  ', bq)
+               # These leading spaces screw with <pre> content, so we need to fix that:
+               bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq)
+
+               return "<blockquote>\n%s\n</blockquote>\n\n" % bq
+
+       def _do_block_quotes(self, text):
+               if '>' not in text:
+                       return text
+               return self._block_quote_re.sub(self._block_quote_sub, text)
+
+       def _form_paragraphs(self, text):
+               # Strip leading and trailing lines:
+               text = text.strip('\n')
+
+               # Wrap <p> tags.
+               grafs = []
+               for i, graf in enumerate(re.split(r"\n{2,}", text)):
+                       if graf in self.html_blocks:
+                               # Unhashify HTML blocks
+                               grafs.append(self.html_blocks[graf])
+                       else:
+                               cuddled_list = None
+                               if "cuddled-lists" in self.extras:
+                                       # Need to put back trailing '\n' for `_list_item_re`
+                                       # match at the end of the paragraph.
+                                       li = self._list_item_re.search(graf + '\n')
+                                       # Two of the same list marker in this paragraph: a likely
+                                       # candidate for a list cuddled to preceding paragraph
+                                       # text (issue 33). Note the `[-1]` is a quick way to
+                                       # consider numeric bullets (e.g. "1." and "2.") to be
+                                       # equal.
+                                       if (li and li.group("next_marker")
+                                               and li.group("marker")[-1] == li.group("next_marker")[-1]):
+                                               start = li.start()
+                                               cuddled_list = self._do_lists(graf[start:]).rstrip("\n")
+                                               assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>")
+                                               graf = graf[:start]
+
+                               # Wrap <p> tags.
+                               graf = self._run_span_gamut(graf)
+                               grafs.append("<p>" + graf.lstrip(" \t") + "</p>")
+
+                               if cuddled_list:
+                                       grafs.append(cuddled_list)
+
+               return "\n\n".join(grafs)
+
+       def _add_footnotes(self, text):
+               if self.footnotes:
+                       footer = [
+                               '<div class="footnotes">',
+                               '<hr' + self.empty_element_suffix,
+                               '<ol>',
+                       ]
+                       for i, id in enumerate(self.footnote_ids):
+                               if i != 0:
+                                       footer.append('')
+                               footer.append('<li id="fn-%s">' % id)
+                               footer.append(self._run_block_gamut(self.footnotes[id]))
+                               backlink = ('<a href="#fnref-%s" '
+                                       'class="footnoteBackLink" '
+                                       'title="Jump back to footnote %d in the text.">'
+                                       '&#8617;</a>' % (id, i+1))
+                               if footer[-1].endswith("</p>"):
+                                       footer[-1] = footer[-1][:-len("</p>")] \
+                                               + '&nbsp;' + backlink + "</p>"
+                               else:
+                                       footer.append("\n<p>%s</p>" % backlink)
+                               footer.append('</li>')
+                       footer.append('</ol>')
+                       footer.append('</div>')
+                       return text + '\n\n' + '\n'.join(footer)
+               else:
+                       return text
+
+       # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
+       #       http://bumppo.net/projects/amputator/
+       _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')
+       _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
+       _naked_gt_re = re.compile(r'''(?<![a-z?!/'"-])>''', re.I)
+
+       def _encode_amps_and_angles(self, text):
+               # Smart processing for ampersands and angle brackets that need
+               # to be encoded.
+               text = self._ampersand_re.sub('&amp;', text)
+
+               # Encode naked <'s
+               text = self._naked_lt_re.sub('&lt;', text)
+
+               # Encode naked >'s
+               # Note: Other markdown implementations (e.g. Markdown.pl, PHP
+               # Markdown) don't do this.
+               text = self._naked_gt_re.sub('&gt;', text)
+               return text
+
+       def _encode_backslash_escapes(self, text):
+               for ch, escape in g_escape_table.items():
+                       text = text.replace("\\"+ch, escape)
+               return text
+
+       _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I)
+       def _auto_link_sub(self, match):
+               g1 = match.group(1)
+               return '<a href="%s">%s</a>' % (g1, g1)
+
+       _auto_email_link_re = re.compile(r"""
+                 <
+                  (?:mailto:)?
+                 (
+                         [-.\w]+
+                         \@
+                         [-\w]+(\.[-\w]+)*\.[a-z]+
+                 )
+                 >
+               """, re.I | re.X | re.U)
+       def _auto_email_link_sub(self, match):
+               return self._encode_email_address(
+                       self._unescape_special_chars(match.group(1)))
+
+       def _do_auto_links(self, text):
+               text = self._auto_link_re.sub(self._auto_link_sub, text)
+               text = self._auto_email_link_re.sub(self._auto_email_link_sub, text)
+               return text
+
+       def _encode_email_address(self, addr):
+               #  Input: an email address, e.g. "foo@example.com"
+               #
+               #  Output: the email address as a mailto link, with each character
+               #          of the address encoded as either a decimal or hex entity, in
+               #          the hopes of foiling most address harvesting spam bots. E.g.:
+               #
+               #        <a href="&#x6D;&#97;&#105;&#108;&#x74;&#111;:&#102;&#111;&#111;&#64;&#101;
+               #               x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;">&#102;&#111;&#111;
+               #               &#64;&#101;x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;</a>
+               #
+               #  Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
+               #  mailing list: <http://tinyurl.com/yu7ue>
+               chars = [_xml_encode_email_char_at_random(ch)
+                                for ch in "mailto:" + addr]
+               # Strip the mailto: from the visible part.
+               addr = '<a href="%s">%s</a>' \
+                          % (''.join(chars), ''.join(chars[7:]))
+               return addr
+
+       def _do_link_patterns(self, text):
+               """Caveat emptor: there isn't much guarding against link
+               patterns being formed inside other standard Markdown links, e.g.
+               inside a [link def][like this].
+
+               Dev Notes: *Could* consider prefixing regexes with a negative
+               lookbehind assertion to attempt to guard against this.
+               """
+               link_from_hash = {}
+               for regex, repl in self.link_patterns:
+                       replacements = []
+                       for match in regex.finditer(text):
+                               if hasattr(repl, "__call__"):
+                                       href = repl(match)
+                               else:
+                                       href = match.expand(repl)
+                               replacements.append((match.span(), href))
+                       for (start, end), href in reversed(replacements):
+                               escaped_href = (
+                                       href.replace('"', '&quot;')      # b/c of attr quote
+                                               # To avoid markdown <em> and <strong>:
+                                               .replace('*', g_escape_table['*'])
+                                               .replace('_', g_escape_table['_']))
+                               link = '<a href="%s">%s</a>' % (escaped_href, text[start:end])
+                               hash = _hash_text(link)
+                               link_from_hash[hash] = link
+                               text = text[:start] + hash + text[end:]
+               for hash, link in link_from_hash.items():
+                       text = text.replace(hash, link)
+               return text
+
+       def _unescape_special_chars(self, text):
+               # Swap back in all the special characters we've hidden.
+               for ch, hash in g_escape_table.items():
+                       text = text.replace(hash, ch)
+               return text
+
+       def _outdent(self, text):
+               # Remove one level of line-leading tabs or spaces
+               return self._outdent_re.sub('', text)
 
 
 class MarkdownWithExtras(Markdown):
 
 
 class MarkdownWithExtras(Markdown):
-    """A markdowner class that enables most extras:
+       """A markdowner class that enables most extras:
 
 
-    - footnotes
-    - code-color (only has effect if 'pygments' Python module on path)
+       - footnotes
+       - code-color (only has effect if 'pygments' Python module on path)
 
 
-    These are not included:
-    - pyshell (specific to Python-related documenting)
-    - code-friendly (because it *disables* part of the syntax)
-    - link-patterns (because you need to specify some actual
-      link-patterns anyway)
-    """
-    extras = ["footnotes", "code-color"]
+       These are not included:
+       - pyshell (specific to Python-related documenting)
+       - code-friendly (because it *disables* part of the syntax)
+       - link-patterns (because you need to specify some actual
+         link-patterns anyway)
+       """
+       extras = ["footnotes", "code-color"]
 
 
 #---- internal support functions
 
 # From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549
 def _curry(*args, **kwargs):
 
 
 #---- internal support functions
 
 # From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549
 def _curry(*args, **kwargs):
-    function, args = args[0], args[1:]
-    def result(*rest, **kwrest):
-        combined = kwargs.copy()
-        combined.update(kwrest)
-        return function(*args + rest, **combined)
-    return result
+       function, args = args[0], args[1:]
+       def result(*rest, **kwrest):
+               combined = kwargs.copy()
+               combined.update(kwrest)
+               return function(*args + rest, **combined)
+       return result
 
 # Recipe: regex_from_encoded_pattern (1.0)
 def _regex_from_encoded_pattern(s):
 
 # Recipe: regex_from_encoded_pattern (1.0)
 def _regex_from_encoded_pattern(s):
-    """'foo'    -> re.compile(re.escape('foo'))
-       '/foo/'  -> re.compile('foo')
-       '/foo/i' -> re.compile('foo', re.I)
-    """
-    if s.startswith('/') and s.rfind('/') != 0:
-        # Parse it: /PATTERN/FLAGS
-        idx = s.rfind('/')
-        pattern, flags_str = s[1:idx], s[idx+1:]
-        flag_from_char = {
-            "i": re.IGNORECASE,
-            "l": re.LOCALE,
-            "s": re.DOTALL,
-            "m": re.MULTILINE,
-            "u": re.UNICODE,
-        }
-        flags = 0
-        for char in flags_str:
-            try:
-                flags |= flag_from_char[char]
-            except KeyError:
-                raise ValueError("unsupported regex flag: '%s' in '%s' "
-                                 "(must be one of '%s')"
-                                 % (char, s, ''.join(flag_from_char.keys())))
-        return re.compile(s[1:idx], flags)
-    else: # not an encoded regex
-        return re.compile(re.escape(s))
+       """'foo'        -> re.compile(re.escape('foo'))
+          '/foo/'      -> re.compile('foo')
+          '/foo/i' -> re.compile('foo', re.I)
+       """
+       if s.startswith('/') and s.rfind('/') != 0:
+               # Parse it: /PATTERN/FLAGS
+               idx = s.rfind('/')
+               pattern, flags_str = s[1:idx], s[idx+1:]
+               flag_from_char = {
+                       "i": re.IGNORECASE,
+                       "l": re.LOCALE,
+                       "s": re.DOTALL,
+                       "m": re.MULTILINE,
+                       "u": re.UNICODE,
+               }
+               flags = 0
+               for char in flags_str:
+                       try:
+                               flags |= flag_from_char[char]
+                       except KeyError:
+                               raise ValueError("unsupported regex flag: '%s' in '%s' "
+                                                                "(must be one of '%s')"
+                                                                % (char, s, ''.join(flag_from_char.keys())))
+               return re.compile(s[1:idx], flags)
+       else: # not an encoded regex
+               return re.compile(re.escape(s))
 
 # Recipe: dedent (0.1.2)
 def _dedentlines(lines, tabsize=8, skip_first_line=False):
 
 # Recipe: dedent (0.1.2)
 def _dedentlines(lines, tabsize=8, skip_first_line=False):
-    """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines
-
-        "lines" is a list of lines to dedent.
-        "tabsize" is the tab width to use for indent width calculations.
-        "skip_first_line" is a boolean indicating if the first line should
-            be skipped for calculating the indent width and for dedenting.
-            This is sometimes useful for docstrings and similar.
-
-    Same as dedent() except operates on a sequence of lines. Note: the
-    lines list is modified **in-place**.
-    """
-    DEBUG = False
-    if DEBUG:
-        print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
-              % (tabsize, skip_first_line)
-    indents = []
-    margin = None
-    for i, line in enumerate(lines):
-        if i == 0 and skip_first_line: continue
-        indent = 0
-        for ch in line:
-            if ch == ' ':
-                indent += 1
-            elif ch == '\t':
-                indent += tabsize - (indent % tabsize)
-            elif ch in '\r\n':
-                continue # skip all-whitespace lines
-            else:
-                break
-        else:
-            continue # skip all-whitespace lines
-        if DEBUG: print "dedent: indent=%d: %r" % (indent, line)
-        if margin is None:
-            margin = indent
-        else:
-            margin = min(margin, indent)
-    if DEBUG: print "dedent: margin=%r" % margin
-
-    if margin is not None and margin > 0:
-        for i, line in enumerate(lines):
-            if i == 0 and skip_first_line: continue
-            removed = 0
-            for j, ch in enumerate(line):
-                if ch == ' ':
-                    removed += 1
-                elif ch == '\t':
-                    removed += tabsize - (removed % tabsize)
-                elif ch in '\r\n':
-                    if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line
-                    lines[i] = lines[i][j:]
-                    break
-                else:
-                    raise ValueError("unexpected non-whitespace char %r in "
-                                     "line %r while removing %d-space margin"
-                                     % (ch, line, margin))
-                if DEBUG:
-                    print "dedent: %r: %r -> removed %d/%d"\
-                          % (line, ch, removed, margin)
-                if removed == margin:
-                    lines[i] = lines[i][j+1:]
-                    break
-                elif removed > margin:
-                    lines[i] = ' '*(removed-margin) + lines[i][j+1:]
-                    break
-            else:
-                if removed:
-                    lines[i] = lines[i][removed:]
-    return lines
+       """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines
+
+               "lines" is a list of lines to dedent.
+               "tabsize" is the tab width to use for indent width calculations.
+               "skip_first_line" is a boolean indicating if the first line should
+                       be skipped for calculating the indent width and for dedenting.
+                       This is sometimes useful for docstrings and similar.
+
+       Same as dedent() except operates on a sequence of lines. Note: the
+       lines list is modified **in-place**.
+       """
+       DEBUG = False
+       if DEBUG:
+               print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
+                         % (tabsize, skip_first_line)
+       indents = []
+       margin = None
+       for i, line in enumerate(lines):
+               if i == 0 and skip_first_line: continue
+               indent = 0
+               for ch in line:
+                       if ch == ' ':
+                               indent += 1
+                       elif ch == '\t':
+                               indent += tabsize - (indent % tabsize)
+                       elif ch in '\r\n':
+                               continue # skip all-whitespace lines
+                       else:
+                               break
+               else:
+                       continue # skip all-whitespace lines
+               if DEBUG: print "dedent: indent=%d: %r" % (indent, line)
+               if margin is None:
+                       margin = indent
+               else:
+                       margin = min(margin, indent)
+       if DEBUG: print "dedent: margin=%r" % margin
+
+       if margin is not None and margin > 0:
+               for i, line in enumerate(lines):
+                       if i == 0 and skip_first_line: continue
+                       removed = 0
+                       for j, ch in enumerate(line):
+                               if ch == ' ':
+                                       removed += 1
+                               elif ch == '\t':
+                                       removed += tabsize - (removed % tabsize)
+                               elif ch in '\r\n':
+                                       if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line
+                                       lines[i] = lines[i][j:]
+                                       break
+                               else:
+                                       raise ValueError("unexpected non-whitespace char %r in "
+                                                                        "line %r while removing %d-space margin"
+                                                                        % (ch, line, margin))
+                               if DEBUG:
+                                       print "dedent: %r: %r -> removed %d/%d"\
+                                                 % (line, ch, removed, margin)
+                               if removed == margin:
+                                       lines[i] = lines[i][j+1:]
+                                       break
+                               elif removed > margin:
+                                       lines[i] = ' '*(removed-margin) + lines[i][j+1:]
+                                       break
+                       else:
+                               if removed:
+                                       lines[i] = lines[i][removed:]
+       return lines
 
 def _dedent(text, tabsize=8, skip_first_line=False):
 
 def _dedent(text, tabsize=8, skip_first_line=False):
-    """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text
+       """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text
 
 
-        "text" is the text to dedent.
-        "tabsize" is the tab width to use for indent width calculations.
-        "skip_first_line" is a boolean indicating if the first line should
-            be skipped for calculating the indent width and for dedenting.
-            This is sometimes useful for docstrings and similar.
+               "text" is the text to dedent.
+               "tabsize" is the tab width to use for indent width calculations.
+               "skip_first_line" is a boolean indicating if the first line should
+                       be skipped for calculating the indent width and for dedenting.
+                       This is sometimes useful for docstrings and similar.
 
 
-    textwrap.dedent(s), but don't expand tabs to spaces
-    """
-    lines = text.splitlines(1)
-    _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)
-    return ''.join(lines)
+       textwrap.dedent(s), but don't expand tabs to spaces
+       """
+       lines = text.splitlines(1)
+       _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)
+       return ''.join(lines)
 
 
 class _memoized(object):
 
 
 class _memoized(object):
@@ -1512,76 +1512,76 @@ class _memoized(object):
    http://wiki.python.org/moin/PythonDecoratorLibrary
    """
    def __init__(self, func):
    http://wiki.python.org/moin/PythonDecoratorLibrary
    """
    def __init__(self, func):
-      self.func = func
-      self.cache = {}
+         self.func = func
+         self.cache = {}
    def __call__(self, *args):
    def __call__(self, *args):
-      try:
-         return self.cache[args]
-      except KeyError:
-         self.cache[args] = value = self.func(*args)
-         return value
-      except TypeError:
-         # uncachable -- for instance, passing a list as an argument.
-         # Better to not cache than to blow up entirely.
-         return self.func(*args)
+         try:
+                return self.cache[args]
+         except KeyError:
+                self.cache[args] = value = self.func(*args)
+                return value
+         except TypeError:
+                # uncachable -- for instance, passing a list as an argument.
+                # Better to not cache than to blow up entirely.
+                return self.func(*args)
    def __repr__(self):
    def __repr__(self):
-      """Return the function's docstring."""
-      return self.func.__doc__
+         """Return the function's docstring."""
+         return self.func.__doc__
 
 
 def _xml_oneliner_re_from_tab_width(tab_width):
 
 
 def _xml_oneliner_re_from_tab_width(tab_width):
-    """Standalone XML processing instruction regex."""
-    return re.compile(r"""
-        (?:
-            (?<=\n\n)       # Starting after a blank line
-            |               # or
-            \A\n?           # the beginning of the doc
-        )
-        (                           # save in $1
-            [ ]{0,%d}
-            (?:
-                <\?\w+\b\s+.*?\?>   # XML processing instruction
-                |
-                <\w+:\w+\b\s+.*?/>  # namespaced single tag
-            )
-            [ \t]*
-            (?=\n{2,}|\Z)       # followed by a blank line or end of document
-        )
-        """ % (tab_width - 1), re.X)
+       """Standalone XML processing instruction regex."""
+       return re.compile(r"""
+               (?:
+                       (?<=\n\n)               # Starting after a blank line
+                       |                               # or
+                       \A\n?                   # the beginning of the doc
+               )
+               (                                                       # save in $1
+                       [ ]{0,%d}
+                       (?:
+                               <\?\w+\b\s+.*?\?>       # XML processing instruction
+                               |
+                               <\w+:\w+\b\s+.*?/>      # namespaced single tag
+                       )
+                       [ \t]*
+                       (?=\n{2,}|\Z)           # followed by a blank line or end of document
+               )
+               """ % (tab_width - 1), re.X)
 _xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width)
 
 def _hr_tag_re_from_tab_width(tab_width):
 _xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width)
 
 def _hr_tag_re_from_tab_width(tab_width):
-     return re.compile(r"""
-        (?:
-            (?<=\n\n)       # Starting after a blank line
-            |               # or
-            \A\n?           # the beginning of the doc
-        )
-        (                       # save in \1
-            [ ]{0,%d}
-            <(hr)               # start tag = \2
-            \b                  # word break
-            ([^<>])*?           #
-            /?>                 # the matching end tag
-            [ \t]*
-            (?=\n{2,}|\Z)       # followed by a blank line or end of document
-        )
-        """ % (tab_width - 1), re.X)
+        return re.compile(r"""
+               (?:
+                       (?<=\n\n)               # Starting after a blank line
+                       |                               # or
+                       \A\n?                   # the beginning of the doc
+               )
+               (                                               # save in \1
+                       [ ]{0,%d}
+                       <(hr)                           # start tag = \2
+                       \b                                      # word break
+                       ([^<>])*?                       #
+                       /?>                                     # the matching end tag
+                       [ \t]*
+                       (?=\n{2,}|\Z)           # followed by a blank line or end of document
+               )
+               """ % (tab_width - 1), re.X)
 _hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width)
 
 
 def _xml_encode_email_char_at_random(ch):
 _hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width)
 
 
 def _xml_encode_email_char_at_random(ch):
-    r = random()
-    # Roughly 10% raw, 45% hex, 45% dec.
-    # '@' *must* be encoded. I [John Gruber] insist.
-    # Issue 26: '_' must be encoded.
-    if r > 0.9 and ch not in "@_":
-        return ch
-    elif r < 0.45:
-        # The [1:] is to drop leading '0': 0x63 -> x63
-        return '&#%s;' % hex(ord(ch))[1:]
-    else:
-        return '&#%s;' % ord(ch)
+       r = random()
+       # Roughly 10% raw, 45% hex, 45% dec.
+       # '@' *must* be encoded. I [John Gruber] insist.
+       # Issue 26: '_' must be encoded.
+       if r > 0.9 and ch not in "@_":
+               return ch
+       elif r < 0.45:
+               # The [1:] is to drop leading '0': 0x63 -> x63
+               return '&#%s;' % hex(ord(ch))[1:]
+       else:
+               return '&#%s;' % ord(ch)
 
 
 
 
 
 
@@ -1597,7 +1597,7 @@ Dies ist ein Text.
 """
 
 #markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
 """
 
 #markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
-#             safe_mode=None, extras=None, link_patterns=None):
+#                        safe_mode=None, extras=None, link_patterns=None):
 #html = markdown(text, html4tags=False)
 #
 #print html
 #html = markdown(text, html4tags=False)
 #
 #print html
index b9eced227a3e0bf87342f4833e52c5d2f68ea53c..eb61cc0308568ca1563d7591546e1d1c4bb13433 100644 (file)
@@ -27,16 +27,16 @@ def checkconfig(params):
 # Helper class needed for datetime.datetime to generate GMT timestamps
 ZERO = datetime.timedelta(0)
 class UTC(datetime.tzinfo):
 # Helper class needed for datetime.datetime to generate GMT timestamps
 ZERO = datetime.timedelta(0)
 class UTC(datetime.tzinfo):
-    """UTC"""
+       """UTC"""
 
 
-    def utcoffset(self, dt):
-        return ZERO
+       def utcoffset(self, dt):
+               return ZERO
 
 
-    def tzname(self, dt):
-        return "UTC"
+       def tzname(self, dt):
+               return "UTC"
 
 
-    def dst(self, dt):
-        return ZERO
+       def dst(self, dt):
+               return ZERO
 utc = UTC()
 
 
 utc = UTC()
 
 
index 5873e53dbd7d2ca7bb408f9a167a4daedfb8f787..9a7345360de06e2da787af3a64469d9ec03bd5ef 100644 (file)
@@ -25,37 +25,37 @@ def slugify(text, separator):
        Based on http://snipplr.com/view/26266/create-slugs-in-python/
        """
 
        Based on http://snipplr.com/view/26266/create-slugs-in-python/
        """
 
-    ret = ""
-    for c in text.lower():
-        try:
-            ret += htmlentitydefs.codepoint2name[ord(c)]
-        except:
-            ret += c
-    ret = re.sub("([a-zA-Z])(uml|acute|grave|circ|tilde|cedil)", r"\1", ret)
-    ret = re.sub("\W", " ", ret)
-    ret = re.sub(" +", separator, ret)
-    return ret.strip()
+       ret = ""
+       for c in text.lower():
+               try:
+                       ret += htmlentitydefs.codepoint2name[ord(c)]
+               except:
+                       ret += c
+       ret = re.sub("([a-zA-Z])(uml|acute|grave|circ|tilde|cedil)", r"\1", ret)
+       ret = re.sub("\W", " ", ret)
+       ret = re.sub(" +", separator, ret)
+       return ret.strip()
 
 
 def repl(m):
 
 
 def repl(m):
-    global toc
-    label = slugify(m.group(3), "_")
-    if labels.has_key(label):
-        n = 0
-        while True:
-            l = "%s_%d" % (label, n)
-            if not labels.has_key(l):
-                label = l
-                break
-            n += 1
-
-    toc.append( (label, int(m.group(1))-1, m.group(3)) )
-    return '<h%s%s>%s<a name="%s">&nbsp;</a></h%s>' % (
-        m.group(1),
-        m.group(2),
-        m.group(3),
-        label,
-        m.group(1))
+       global toc
+       label = slugify(m.group(3), "_")
+       if labels.has_key(label):
+               n = 0
+               while True:
+                       l = "%s_%d" % (label, n)
+                       if not labels.has_key(l):
+                               label = l
+                               break
+                       n += 1
+
+       toc.append( (label, int(m.group(1))-1, m.group(3)) )
+       return '<h%s%s>%s<a name="%s">&nbsp;</a></h%s>' % (
+               m.group(1),
+               m.group(2),
+               m.group(3),
+               label,
+               m.group(1))
        """
        Function used for re.sub() to find all header elements (h1, h2, ...).
        Data from those elements (level, headline) are stored in the global
        """
        Function used for re.sub() to find all header elements (h1, h2, ...).
        Data from those elements (level, headline) are stored in the global
@@ -69,31 +69,32 @@ def repl(m):
 
 @set_hook("linkify")
 def linkify(params):
 
 @set_hook("linkify")
 def linkify(params):
-    global toc
-    global labels
-    toc = []
-    labels = {}
+       global toc
+       global labels
+       toc = []
+       labels = {}
 
 
-    # Very small pages don't need a table-of-contents
-    if params.file.contents.count("\n") < toc_min_lines:
-        return
+       # Very small pages don't need a table-of-contents
+       if params.file.contents.count("\n") < toc_min_lines:
+               return
 
 
-    params.file.contents = reHeader.sub(repl, params.file.contents)
+       params.file.contents = reHeader.sub(repl, params.file.contents)
 
 
 
 @set_function("get_toc")
 def get_toc():
 
 
 
 @set_function("get_toc")
 def get_toc():
-    level = 1
-    res = []
-    for (label, lvl, txt) in toc:
-        while lvl > level:
-            res.append("%s<ul>" % ("  "*level))
-            level += 1
-        while lvl < level:
-            level -= 1
-            res.append("%s</ul>" % ("  "*level))
-        res.append('%s<li><a href="#%s">%s</a></li>' % ("  " * level, label, txt))
-    while level > 1:
-        level -= 1
-    return "\n".join(res)
+       level = 1
+       res = []
+       for (label, lvl, txt) in toc:
+               while lvl > level:
+                       res.append("%s<ul>" % ("  "*level))
+                       level += 1
+               while lvl < level:
+                       level -= 1
+                       res.append("%s</ul>" % ("  "*level))
+               res.append('%s<li><a href="#%s">%s</a></li>' %
+                          ("  " * level, label, txt))
+       while level > 1:
+               level -= 1
+       return "\n".join(res)
index 6be571e0ff09ea19d88e0e9e2ef6625b442eaf16..07136de40733244f7f26a0ce75918834d02c012b 100644 (file)
--- a/webber.py
+++ b/webber.py
@@ -11,21 +11,21 @@ from config import Holder
 
 __all__ = [
        # Globals
 
 __all__ = [
        # Globals
-       "cfg",          # configuration from webber.ini
-       "directories",  # global hash of directories, by rel_path
-       "files",        # global hash of files, by rel_path
-       "functions",    # all exported template functions
+       "cfg",                  # configuration from webber.ini
+       "directories",  # global hash of directories, by rel_path
+       "files",                # global hash of files, by rel_path
+       "functions",    # all exported template functions
 
        # Functions
 
        # Functions
-       "set_hook",     # decorator for hook-functions
-       "set_macro",    # define macro
+       "set_hook",             # decorator for hook-functions
+       "set_macro",    # define macro
        "set_function", # define functions for the template
        "get_file_for",
        "get_link_from",
        "get_current_file", # because mako-called functions cannot access the
        "set_function", # define functions for the template
        "get_file_for",
        "get_link_from",
        "get_current_file", # because mako-called functions cannot access the
-                       # current File object
+                                       # current File object
        "get_program_directory",
        "get_program_directory",
-       "log",          # misc logging functions
+       "log",                  # misc logging functions
        "info",
        "warning",
        "error",
        "info",
        "warning",
        "error",
@@ -229,7 +229,7 @@ def get_link_from(source, dest):
        if rel_path.startswith("./"):
                rel_path = rel_path[2:]
        #print "  from path:", source.out_path
        if rel_path.startswith("./"):
                rel_path = rel_path[2:]
        #print "  from path:", source.out_path
-       #print "  to path:  ", out_path
+       #print "  to path:      ", out_path
        #print "  rel path: ", rel_path
        return rel_path
 
        #print "  rel path: ", rel_path
        return rel_path
 
@@ -255,10 +255,10 @@ def get_program_directory():
 #
 #  Logging
 #
 #
 #  Logging
 #
-#      1    Error
-#      2    Warning
-#      3    Info
-#      4    Log
+#      1        Error
+#      2        Warning
+#      3        Info
+#      4        Log
 #      5... Debug
 #
 def log(s, level=4):
 #      5... Debug
 #
 def log(s, level=4):
@@ -288,43 +288,43 @@ def info(s):
 
 # IkiWiki does something like this:
 # At startup:
 
 # IkiWiki does something like this:
 # At startup:
-#      getopt               modify ARGV
-#      checkconfig          check configuration
-#      refresh              allow plugins to build source files
+#      getopt                           modify ARGV
+#      checkconfig                      check configuration
+#      refresh                          allow plugins to build source files
 # While scanning files:
 # While scanning files:
-#      needsbuild           detect if page needs to be rebuild
-#      filter               arbitrary changes
-#      scan                 collect metadata
+#      needsbuild                       detect if page needs to be rebuild
+#      filter                           arbitrary changes
+#      scan                             collect metadata
 # While rendering files:
 # While rendering files:
-#      filter               arbitrary changes
-#      preprocess           execute macros
-#      linkify              change wikilinks into links
-#      htmlize              turns text into html
-#      sanitize             sanitize html
-#      templatefile         allows changing of the template on a per-file basis
-#      pagetemplate         fill template with page
-#      format               similar to sanitize, but act on whole page body
+#      filter                           arbitrary changes
+#      preprocess                       execute macros
+#      linkify                          change wikilinks into links
+#      htmlize                          turns text into html
+#      sanitize                         sanitize html
+#      templatefile             allows changing of the template on a per-file basis
+#      pagetemplate             fill template with page
+#      format                           similar to sanitize, but act on whole page body
 # At the end:
 # At the end:
-#      savestate            plugins can save their state
+#      savestate                        plugins can save their state
 #
 #
 # We do something like this:
 #
 # At startup:
 #
 #
 # We do something like this:
 #
 # At startup:
-#      addoptions           allow plugins to add command-line options
-#      checkconfig          check configuration
+#      addoptions                       allow plugins to add command-line options
+#      checkconfig                      check configuration
 #      start
 # While reading files:
 #      start
 # While reading files:
-#      read                 ask any reader (plugins!) to read the file
-#      filter               ask anybody to filter the contents
+#      read                             ask any reader (plugins!) to read the file
+#      filter                           ask anybody to filter the contents
 # While scanning files:
 # While scanning files:
-#      scan                 called per file, let plugins act on file data
-#      scan_done            Allows post-processing of scanned data
+#      scan                             called per file, let plugins act on file data
+#      scan_done                        Allows post-processing of scanned data
 # While rendering files:
 # While rendering files:
-#      htmlize              turns text into html-part
-#      linkify              convert link macros to HTML
-#      pagetemplate         ask template engine (plugin!) to generate HTML out
-#                           of template and body part
+#      htmlize                          turns text into html-part
+#      linkify                          convert link macros to HTML
+#      pagetemplate             ask template engine (plugin!) to generate HTML out
+#                                               of template and body part
 # At the end:
 #      finish
 #
 # At the end:
 #      finish
 #
@@ -554,14 +554,14 @@ def walk_tree(dirpath):
 #
 
 reMacro = re.compile(r'''
 #
 
 reMacro = re.compile(r'''
-       \[\[\!                  # Begin of macro
+       \[\[\!                                  # Begin of macro
        \s*
        \s*
-       ([^\s\]]+)              # Macro name
+       ([^\s\]]+)                              # Macro name
        (?:
        (?:
-               \s+                 # optional space
-        ([^\]]+)            # optional argumens
+               \s+                                     # optional space
+               ([^\]]+)                        # optional argumens
        )?
        )?
-       \]\]                    # End of macro
+       \]\]                                    # End of macro
        ''', re.VERBOSE)
 reMacroArgs = re.compile(r'''
        ([-_\w]+)                               # parameter name
        ''', re.VERBOSE)
 reMacroArgs = re.compile(r'''
        ([-_\w]+)                               # parameter name
@@ -570,9 +570,9 @@ reMacroArgs = re.compile(r'''
                =
                \s*
                (?:
                =
                \s*
                (?:
-                       "([^"]*)"       # single-quoted
+                       "([^"]*)"               # single-quoted
                |
                |
-                       (\S+)           # unquoted
+                       (\S+)                   # unquoted
                )
        )?
        ''', re.VERBOSE)
                )
        )?
        ''', re.VERBOSE)