Skip to content

Commit

Permalink
Replace homegrown OrderedDict with purpose-built Registry. (#688)
Browse files Browse the repository at this point in the history
All processors and patterns now get "registered" to a Registry.
Each item is given a name (string) and a priority. The name is for
later reference and the priority can be either an integer or float
and is used to sort. Priority is sorted from highest to lowest. A 
Registry instance is a list-like iterable with the items auto-sorted 
by priority. If two items have the same priority, then they are 
listed in the order there were "registered". Registering a new 
item with the same name as an already registered item replaces
the old item with the new item (however, the new item is sorted by
its newly assigned priority). To remove an item, "deregister" it by 
name or index.

A backwards compatible shim is included so that existing simple
extensions should continue to work. DeprecationWarnings will 
be raised for any code which calls the old API.

Fixes #418.
  • Loading branch information
waylan committed Jul 27, 2018
1 parent 5d2cde2 commit 6ee07d2
Show file tree
Hide file tree
Showing 27 changed files with 528 additions and 480 deletions.
5 changes: 2 additions & 3 deletions markdown/blockparser.py
@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from . import util
from . import odict


class State(list):
Expand Down Expand Up @@ -46,7 +45,7 @@ class BlockParser:
"""

def __init__(self, markdown):
self.blockprocessors = odict.OrderedDict()
self.blockprocessors = util.Registry()
self.state = State()
self.markdown = markdown

Expand Down Expand Up @@ -93,7 +92,7 @@ def parseBlocks(self, parent, blocks):
"""
while blocks:
for processor in self.blockprocessors.values():
for processor in self.blockprocessors:
if processor.test(parent, blocks[0]):
if processor.run(parent, blocks) is not False:
# run returns True or None
Expand Down
20 changes: 10 additions & 10 deletions markdown/blockprocessors.py
Expand Up @@ -25,16 +25,16 @@
def build_block_parser(md_instance, **kwargs):
""" Build the default block parser used by Markdown. """
parser = BlockParser(md_instance)
parser.blockprocessors['empty'] = EmptyBlockProcessor(parser)
parser.blockprocessors['indent'] = ListIndentProcessor(parser)
parser.blockprocessors['code'] = CodeBlockProcessor(parser)
parser.blockprocessors['hashheader'] = HashHeaderProcessor(parser)
parser.blockprocessors['setextheader'] = SetextHeaderProcessor(parser)
parser.blockprocessors['hr'] = HRProcessor(parser)
parser.blockprocessors['olist'] = OListProcessor(parser)
parser.blockprocessors['ulist'] = UListProcessor(parser)
parser.blockprocessors['quote'] = BlockQuoteProcessor(parser)
parser.blockprocessors['paragraph'] = ParagraphProcessor(parser)
parser.blockprocessors.register(EmptyBlockProcessor(parser), 'empty', 100)
parser.blockprocessors.register(ListIndentProcessor(parser), 'indent', 90)
parser.blockprocessors.register(CodeBlockProcessor(parser), 'code', 80)
parser.blockprocessors.register(HashHeaderProcessor(parser), 'hashheader', 70)
parser.blockprocessors.register(SetextHeaderProcessor(parser), 'setextheader', 60)
parser.blockprocessors.register(HRProcessor(parser), 'hr', 50)
parser.blockprocessors.register(OListProcessor(parser), 'olist', 40)
parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30)
parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20)
parser.blockprocessors.register(ParagraphProcessor(parser), 'paragraph', 10)
return parser


Expand Down
6 changes: 3 additions & 3 deletions markdown/core.py
Expand Up @@ -230,14 +230,14 @@ def convert(self, source):

# Split into lines and run the line preprocessors.
self.lines = source.split("\n")
for prep in self.preprocessors.values():
for prep in self.preprocessors:
self.lines = prep.run(self.lines)

# Parse the high-level elements.
root = self.parser.parseDocument(self.lines).getroot()

# Run the tree-processors
for treeprocessor in self.treeprocessors.values():
for treeprocessor in self.treeprocessors:
newRoot = treeprocessor.run(root)
if newRoot is not None:
root = newRoot
Expand All @@ -260,7 +260,7 @@ def convert(self, source):
'tags. Document=%r' % output.strip())

# Run the text post-processors
for pp in self.postprocessors.values():
for pp in self.postprocessors:
output = pp.run(output)

return output.strip()
Expand Down
7 changes: 4 additions & 3 deletions markdown/extensions/abbr.py
Expand Up @@ -33,7 +33,7 @@ class AbbrExtension(Extension):

def extendMarkdown(self, md, md_globals):
""" Insert AbbrPreprocessor before ReferencePreprocessor. """
md.preprocessors.add('abbr', AbbrPreprocessor(md), '<reference')
md.preprocessors.register(AbbrPreprocessor(md), 'abbr', 12)


class AbbrPreprocessor(Preprocessor):
Expand All @@ -51,8 +51,9 @@ def run(self, lines):
if m:
abbr = m.group('abbr').strip()
title = m.group('title').strip()
self.markdown.inlinePatterns['abbr-%s' % abbr] = \
AbbrInlineProcessor(self._generate_pattern(abbr), title)
self.markdown.inlinePatterns.register(
AbbrInlineProcessor(self._generate_pattern(abbr), title), 'abbr-%s' % abbr, 2
)
# Preserve the line to prevent raw HTML indexing issue.
# https://github.com/Python-Markdown/markdown/issues/584
new_text.append('')
Expand Down
4 changes: 1 addition & 3 deletions markdown/extensions/admonition.py
Expand Up @@ -32,9 +32,7 @@ def extendMarkdown(self, md, md_globals):
""" Add Admonition to Markdown instance. """
md.registerExtension(self)

md.parser.blockprocessors.add('admonition',
AdmonitionProcessor(md.parser),
'_begin')
md.parser.blockprocessors.register(AdmonitionProcessor(md.parser), 'admonition', 105)


class AdmonitionProcessor(BlockProcessor):
Expand Down
4 changes: 1 addition & 3 deletions markdown/extensions/attr_list.py
Expand Up @@ -163,9 +163,7 @@ def sanitize_name(self, name):

class AttrListExtension(Extension):
def extendMarkdown(self, md, md_globals):
md.treeprocessors.add(
'attr_list', AttrListTreeprocessor(md), '>prettify'
)
md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8)


def makeExtension(**kwargs): # pragma: no cover
Expand Down
2 changes: 1 addition & 1 deletion markdown/extensions/codehilite.py
Expand Up @@ -255,7 +255,7 @@ def extendMarkdown(self, md, md_globals):
""" Add HilitePostprocessor to Markdown instance. """
hiliter = HiliteTreeprocessor(md)
hiliter.config = self.getConfigs()
md.treeprocessors.add("hilite", hiliter, "<inline")
md.treeprocessors.register(hiliter, 'hilite', 30)

md.registerExtension(self)

Expand Down
8 changes: 2 additions & 6 deletions markdown/extensions/def_list.py
Expand Up @@ -103,12 +103,8 @@ class DefListExtension(Extension):

def extendMarkdown(self, md, md_globals):
""" Add an instance of DefListProcessor to BlockParser. """
md.parser.blockprocessors.add('defindent',
DefListIndentProcessor(md.parser),
'>indent')
md.parser.blockprocessors.add('deflist',
DefListProcessor(md.parser),
'>ulist')
md.parser.blockprocessors.register(DefListIndentProcessor(md.parser), 'defindent', 85)
md.parser.blockprocessors.register(DefListProcessor(md.parser), 'deflist', 25)


def makeExtension(**kwargs): # pragma: no cover
Expand Down
6 changes: 3 additions & 3 deletions markdown/extensions/extra.py
Expand Up @@ -59,9 +59,9 @@ def extendMarkdown(self, md, md_globals):
md.registerExtensions(extensions, self.config)
# Turn on processing of markdown text within raw html
md.preprocessors['html_block'].markdown_in_raw = True
md.parser.blockprocessors.add('markdown_block',
MarkdownInHtmlProcessor(md.parser),
'_begin')
md.parser.blockprocessors.register(
MarkdownInHtmlProcessor(md.parser), 'markdown_block', 105
)
md.parser.blockprocessors.tag_counter = -1
md.parser.blockprocessors.contain_span_tags = re.compile(
r'^(p|h[1-6]|li|dd|dt|td|th|legend|address)$', re.IGNORECASE)
Expand Down
4 changes: 1 addition & 3 deletions markdown/extensions/fenced_code.py
Expand Up @@ -29,9 +29,7 @@ def extendMarkdown(self, md, md_globals):
""" Add FencedBlockPreprocessor to the Markdown instance. """
md.registerExtension(self)

md.preprocessors.add('fenced_code_block',
FencedBlockPreprocessor(md),
">normalize_whitespace")
md.preprocessors.register(FencedBlockPreprocessor(md), 'fenced_code_block', 25)


class FencedBlockPreprocessor(Preprocessor):
Expand Down
32 changes: 11 additions & 21 deletions markdown/extensions/footnotes.py
Expand Up @@ -21,7 +21,7 @@
from ..treeprocessors import Treeprocessor
from ..postprocessors import Postprocessor
from .. import util
from ..odict import OrderedDict
from collections import OrderedDict
import re
import copy

Expand Down Expand Up @@ -71,33 +71,24 @@ def extendMarkdown(self, md, md_globals):
self.parser = md.parser
self.md = md
# Insert a preprocessor before ReferencePreprocessor
md.preprocessors.add(
"footnote", FootnotePreprocessor(self), "<reference"
)
md.preprocessors.register(FootnotePreprocessor(self), 'footnote', 15)

# Insert an inline pattern before ImageReferencePattern
FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah
md.inlinePatterns.add(
"footnote", FootnoteInlineProcessor(FOOTNOTE_RE, self), "<reference"
)
md.inlinePatterns.register(FootnoteInlineProcessor(FOOTNOTE_RE, self), 'footnote', 175)
# Insert a tree-processor that would actually add the footnote div
# This must be before all other treeprocessors (i.e., inline and
# codehilite) so they can run on the the contents of the div.
md.treeprocessors.add(
"footnote", FootnoteTreeprocessor(self), "_begin"
)
md.treeprocessors.register(FootnoteTreeprocessor(self), 'footnote', 50)

# Insert a tree-processor that will run after inline is done.
# In this tree-processor we want to check our duplicate footnote tracker
# And add additional backrefs to the footnote pointing back to the
# duplicated references.
md.treeprocessors.add(
"footnote-duplicate", FootnotePostTreeprocessor(self), '>inline'
)
md.treeprocessors.register(FootnotePostTreeprocessor(self), 'footnote-duplicate', 15)

# Insert a postprocessor after amp_substitute oricessor
md.postprocessors.add(
"footnote", FootnotePostprocessor(self), ">amp_substitute"
)
# Insert a postprocessor after amp_substitute processor
md.postprocessors.register(FootnotePostprocessor(self), 'footnote', 25)

def reset(self):
""" Clear footnotes on reset, and prepare for distinct document. """
Expand Down Expand Up @@ -180,7 +171,7 @@ def makeFootnotesDiv(self, root):
ol = util.etree.SubElement(div, "ol")
surrogate_parent = util.etree.Element("div")

for id in self.footnotes.keys():
for index, id in enumerate(self.footnotes.keys(), start=1):
li = util.etree.SubElement(ol, "li")
li.set("id", self.makeFootnoteId(id))
# Parse footnote with surrogate parent as li cannot be used.
Expand All @@ -197,8 +188,7 @@ def makeFootnotesDiv(self, root):
backlink.set("class", "footnote-backref")
backlink.set(
"title",
self.getConfig("BACKLINK_TITLE") %
(self.footnotes.index(id)+1)
self.getConfig("BACKLINK_TITLE") % (index)
)
backlink.text = FN_BACKLINK_TEXT

Expand Down Expand Up @@ -332,7 +322,7 @@ def handleMatch(self, m, data):
if self.footnotes.md.output_format not in ['html5', 'xhtml5']:
a.set('rel', 'footnote') # invalid in HTML5
a.set('class', 'footnote-ref')
a.text = util.text_type(self.footnotes.footnotes.index(id) + 1)
a.text = util.text_type(list(self.footnotes.footnotes.keys()).index(id) + 1)
return sup, m.start(0), m.end(0)
else:
return None, None, None
Expand Down
3 changes: 1 addition & 2 deletions markdown/extensions/legacy_attrs.py
Expand Up @@ -41,8 +41,7 @@ def attributeCallback(match):

class LegacyAttrExtension(Extension):
def extendMarkdown(self, md, md_globals):
la = LegacyAttrs(md)
md.treeprocessors.add('legacyattrs', la, '>inline')
md.treeprocessors.register(LegacyAttrs(md), 'legacyattrs', 15)


def makeExtension(**kwargs): # pragma: no cover
Expand Down
4 changes: 1 addition & 3 deletions markdown/extensions/meta.py
Expand Up @@ -38,9 +38,7 @@ def extendMarkdown(self, md, md_globals):
""" Add MetaPreprocessor to Markdown instance. """
md.registerExtension(self)
self.md = md
md.preprocessors.add("meta",
MetaPreprocessor(md),
">normalize_whitespace")
md.preprocessors.register(MetaPreprocessor(md), 'meta', 27)

def reset(self):
self.md.Meta = {}
Expand Down
2 changes: 1 addition & 1 deletion markdown/extensions/nl2br.py
Expand Up @@ -28,7 +28,7 @@ class Nl2BrExtension(Extension):

def extendMarkdown(self, md, md_globals):
br_tag = SubstituteTagInlineProcessor(BR_RE, 'br')
md.inlinePatterns.add('nl', br_tag, '_end')
md.inlinePatterns.register(br_tag, 'nl', 5)


def makeExtension(**kwargs): # pragma: no cover
Expand Down
4 changes: 2 additions & 2 deletions markdown/extensions/sane_lists.py
Expand Up @@ -47,8 +47,8 @@ class SaneListExtension(Extension):

def extendMarkdown(self, md, md_globals):
""" Override existing Processors. """
md.parser.blockprocessors['olist'] = SaneOListProcessor(md.parser)
md.parser.blockprocessors['ulist'] = SaneUListProcessor(md.parser)
md.parser.blockprocessors.register(SaneOListProcessor(md.parser), 'olist', 40)
md.parser.blockprocessors.register(SaneUListProcessor(md.parser), 'ulist', 30)


def makeExtension(**kwargs): # pragma: no cover
Expand Down
8 changes: 2 additions & 6 deletions markdown/extensions/smart_strong.py
Expand Up @@ -29,12 +29,8 @@ class SmartEmphasisExtension(Extension):

def extendMarkdown(self, md, md_globals):
""" Modify inline patterns. """
md.inlinePatterns['strong'] = SimpleTagInlineProcessor(STRONG_RE, 'strong')
md.inlinePatterns.add(
'strong2',
SimpleTagInlineProcessor(SMART_STRONG_RE, 'strong'),
'>emphasis2'
)
md.inlinePatterns.register(SimpleTagInlineProcessor(STRONG_RE, 'strong'), 'strong', 40)
md.inlinePatterns.register(SimpleTagInlineProcessor(SMART_STRONG_RE, 'strong'), 'strong2', 10)


def makeExtension(**kwargs): # pragma: no cover
Expand Down
33 changes: 12 additions & 21 deletions markdown/extensions/smarty.py
Expand Up @@ -84,8 +84,8 @@
from __future__ import unicode_literals
from . import Extension
from ..inlinepatterns import HtmlInlineProcessor, HTML_RE
from ..odict import OrderedDict
from ..treeprocessors import InlineProcessor
from ..util import Registry


# Constants for quote education.
Expand Down Expand Up @@ -180,13 +180,12 @@ def __init__(self, **kwargs):
self.substitutions = dict(substitutions)
self.substitutions.update(self.getConfig('substitutions', default={}))

def _addPatterns(self, md, patterns, serie):
def _addPatterns(self, md, patterns, serie, priority):
for ind, pattern in enumerate(patterns):
pattern += (md,)
pattern = SubstituteTextPattern(*pattern)
after = ('>smarty-%s-%d' % (serie, ind - 1) if ind else '_begin')
name = 'smarty-%s-%d' % (serie, ind)
self.inlinePatterns.add(name, pattern, after)
self.inlinePatterns.register(pattern, name, priority-ind)

def educateDashes(self, md):
emDashesPattern = SubstituteTextPattern(
Expand All @@ -195,16 +194,14 @@ def educateDashes(self, md):
enDashesPattern = SubstituteTextPattern(
r'(?<!-)--(?!-)', (self.substitutions['ndash'],), md
)
self.inlinePatterns.add('smarty-em-dashes', emDashesPattern, '_begin')
self.inlinePatterns.add(
'smarty-en-dashes', enDashesPattern, '>smarty-em-dashes'
)
self.inlinePatterns.register(emDashesPattern, 'smarty-em-dashes', 50)
self.inlinePatterns.register(enDashesPattern, 'smarty-en-dashes', 45)

def educateEllipses(self, md):
ellipsesPattern = SubstituteTextPattern(
r'(?<!\.)\.{3}(?!\.)', (self.substitutions['ellipsis'],), md
)
self.inlinePatterns.add('smarty-ellipses', ellipsesPattern, '_begin')
self.inlinePatterns.register(ellipsesPattern, 'smarty-ellipses', 10)

def educateAngledQuotes(self, md):
leftAngledQuotePattern = SubstituteTextPattern(
Expand All @@ -213,14 +210,8 @@ def educateAngledQuotes(self, md):
rightAngledQuotePattern = SubstituteTextPattern(
r'\>\>', (self.substitutions['right-angle-quote'],), md
)
self.inlinePatterns.add(
'smarty-left-angle-quotes', leftAngledQuotePattern, '_begin'
)
self.inlinePatterns.add(
'smarty-right-angle-quotes',
rightAngledQuotePattern,
'>smarty-left-angle-quotes'
)
self.inlinePatterns.register(leftAngledQuotePattern, 'smarty-left-angle-quotes', 40)
self.inlinePatterns.register(rightAngledQuotePattern, 'smarty-right-angle-quotes', 35)

def educateQuotes(self, md):
lsquo = self.substitutions['left-single-quote']
Expand All @@ -242,11 +233,11 @@ def educateQuotes(self, md):
(closingDoubleQuotesRegex2, (rdquo,)),
(remainingDoubleQuotesRegex, (ldquo,))
)
self._addPatterns(md, patterns, 'quotes')
self._addPatterns(md, patterns, 'quotes', 30)

def extendMarkdown(self, md, md_globals):
configs = self.getConfigs()
self.inlinePatterns = OrderedDict()
self.inlinePatterns = Registry()
if configs['smart_ellipses']:
self.educateEllipses(md)
if configs['smart_quotes']:
Expand All @@ -255,12 +246,12 @@ def extendMarkdown(self, md, md_globals):
self.educateAngledQuotes(md)
# Override HTML_RE from inlinepatterns.py so that it does not
# process tags with duplicate closing quotes.
md.inlinePatterns["html"] = HtmlInlineProcessor(HTML_STRICT_RE, md)
md.inlinePatterns.register(HtmlInlineProcessor(HTML_STRICT_RE, md), 'html', 90)
if configs['smart_dashes']:
self.educateDashes(md)
inlineProcessor = InlineProcessor(md)
inlineProcessor.inlinePatterns = self.inlinePatterns
md.treeprocessors.add('smarty', inlineProcessor, '_end')
md.treeprocessors.register(inlineProcessor, 'smarty', 2)
md.ESCAPED_CHARS.extend(['"', "'"])


Expand Down

0 comments on commit 6ee07d2

Please sign in to comment.