diff options
27 files changed, 528 insertions, 480 deletions
diff --git a/markdown/blockparser.py b/markdown/blockparser.py index 32d3254..5e9d567 100644 --- a/markdown/blockparser.py +++ b/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): @@ -46,7 +45,7 @@ class BlockParser: """ def __init__(self, markdown): - self.blockprocessors = odict.OrderedDict() + self.blockprocessors = util.Registry() self.state = State() self.markdown = markdown @@ -93,7 +92,7 @@ class BlockParser: """ 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 diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index db9add5..50c4591 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -25,16 +25,16 @@ logger = logging.getLogger('MARKDOWN') 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 diff --git a/markdown/core.py b/markdown/core.py index 4b8d1a6..06bf262 100644 --- a/markdown/core.py +++ b/markdown/core.py @@ -230,14 +230,14 @@ class Markdown(object): # 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 @@ -260,7 +260,7 @@ class Markdown(object): '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() diff --git a/markdown/extensions/abbr.py b/markdown/extensions/abbr.py index 5e8845b..a3d456f 100644 --- a/markdown/extensions/abbr.py +++ b/markdown/extensions/abbr.py @@ -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): @@ -51,8 +51,9 @@ class AbbrPreprocessor(Preprocessor): 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('') diff --git a/markdown/extensions/admonition.py b/markdown/extensions/admonition.py index b001957..cf1b506 100644 --- a/markdown/extensions/admonition.py +++ b/markdown/extensions/admonition.py @@ -32,9 +32,7 @@ class AdmonitionExtension(Extension): """ 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): diff --git a/markdown/extensions/attr_list.py b/markdown/extensions/attr_list.py index 76b7d99..a63a5ea 100644 --- a/markdown/extensions/attr_list.py +++ b/markdown/extensions/attr_list.py @@ -163,9 +163,7 @@ class AttrListTreeprocessor(Treeprocessor): 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 diff --git a/markdown/extensions/codehilite.py b/markdown/extensions/codehilite.py index fed4d64..8b9cd8f 100644 --- a/markdown/extensions/codehilite.py +++ b/markdown/extensions/codehilite.py @@ -255,7 +255,7 @@ class CodeHiliteExtension(Extension): """ 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) diff --git a/markdown/extensions/def_list.py b/markdown/extensions/def_list.py index a79c1c1..9463d3a 100644 --- a/markdown/extensions/def_list.py +++ b/markdown/extensions/def_list.py @@ -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 diff --git a/markdown/extensions/extra.py b/markdown/extensions/extra.py index d1294e0..da4cb38 100644 --- a/markdown/extensions/extra.py +++ b/markdown/extensions/extra.py @@ -59,9 +59,9 @@ class ExtraExtension(Extension): 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) diff --git a/markdown/extensions/fenced_code.py b/markdown/extensions/fenced_code.py index e1a616e..c38dabf 100644 --- a/markdown/extensions/fenced_code.py +++ b/markdown/extensions/fenced_code.py @@ -29,9 +29,7 @@ class FencedCodeExtension(Extension): """ 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): diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index a957278..2b9cc40 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -21,7 +21,7 @@ from ..inlinepatterns import InlineProcessor from ..treeprocessors import Treeprocessor from ..postprocessors import Postprocessor from .. import util -from ..odict import OrderedDict +from collections import OrderedDict import re import copy @@ -71,33 +71,24 @@ class FootnoteExtension(Extension): 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. """ @@ -180,7 +171,7 @@ class FootnoteExtension(Extension): 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. @@ -197,8 +188,7 @@ class FootnoteExtension(Extension): 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 @@ -332,7 +322,7 @@ class FootnoteInlineProcessor(InlineProcessor): 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 diff --git a/markdown/extensions/legacy_attrs.py b/markdown/extensions/legacy_attrs.py index b28223f..740f9d6 100644 --- a/markdown/extensions/legacy_attrs.py +++ b/markdown/extensions/legacy_attrs.py @@ -41,8 +41,7 @@ class LegacyAttrs(Treeprocessor): 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 diff --git a/markdown/extensions/meta.py b/markdown/extensions/meta.py index 2c2c8e3..27adcb2 100644 --- a/markdown/extensions/meta.py +++ b/markdown/extensions/meta.py @@ -38,9 +38,7 @@ class MetaExtension (Extension): """ 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 = {} diff --git a/markdown/extensions/nl2br.py b/markdown/extensions/nl2br.py index 5b9373f..d334b02 100644 --- a/markdown/extensions/nl2br.py +++ b/markdown/extensions/nl2br.py @@ -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 diff --git a/markdown/extensions/sane_lists.py b/markdown/extensions/sane_lists.py index 89f929f..7fb4fd6 100644 --- a/markdown/extensions/sane_lists.py +++ b/markdown/extensions/sane_lists.py @@ -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 diff --git a/markdown/extensions/smart_strong.py b/markdown/extensions/smart_strong.py index f34531d..e7a15d9 100644 --- a/markdown/extensions/smart_strong.py +++ b/markdown/extensions/smart_strong.py @@ -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 diff --git a/markdown/extensions/smarty.py b/markdown/extensions/smarty.py index 189651f..d25620b 100644 --- a/markdown/extensions/smarty.py +++ b/markdown/extensions/smarty.py @@ -84,8 +84,8 @@ smartypants.py license: 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. @@ -180,13 +180,12 @@ class SmartyExtension(Extension): 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( @@ -195,16 +194,14 @@ class SmartyExtension(Extension): 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( @@ -213,14 +210,8 @@ class SmartyExtension(Extension): 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'] @@ -242,11 +233,11 @@ class SmartyExtension(Extension): (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']: @@ -255,12 +246,12 @@ class SmartyExtension(Extension): 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(['"', "'"]) diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py index b8218b0..0f221a6 100644 --- a/markdown/extensions/tables.py +++ b/markdown/extensions/tables.py @@ -218,9 +218,7 @@ class TableExtension(Extension): """ Add an instance of TableProcessor to BlockParser. """ if '|' not in md.ESCAPED_CHARS: md.ESCAPED_CHARS.append('|') - md.parser.blockprocessors.add('table', - TableProcessor(md.parser), - '<hashheader') + md.parser.blockprocessors.register(TableProcessor(md.parser), 'table', 75) def makeExtension(**kwargs): # pragma: no cover diff --git a/markdown/extensions/toc.py b/markdown/extensions/toc.py index 5783f3d..f5f2b2e 100644 --- a/markdown/extensions/toc.py +++ b/markdown/extensions/toc.py @@ -218,9 +218,10 @@ class TocTreeprocessor(Treeprocessor): return ul build_etree_ul(toc_list, div) - prettify = self.markdown.treeprocessors.get('prettify') - if prettify: - prettify.run(div) + + if 'prettify' in self.markdown.treeprocessors: + self.markdown.treeprocessors['prettify'].run(div) + return div def run(self, doc): @@ -260,7 +261,7 @@ class TocTreeprocessor(Treeprocessor): # serialize and attach to markdown instance. toc = self.markdown.serializer(div) - for pp in self.markdown.postprocessors.values(): + for pp in self.markdown.postprocessors: toc = pp.run(toc) self.markdown.toc = toc @@ -305,7 +306,7 @@ class TocExtension(Extension): # by the header id extension) if both are used. Same goes for # attr_list extension. This must come last because we don't want # to redefine ids after toc is created. But we do want toc prettified. - md.treeprocessors.add("toc", tocext, "_end") + md.treeprocessors.register(tocext, 'toc', 5) def reset(self): self.md.toc = '' diff --git a/markdown/extensions/wikilinks.py b/markdown/extensions/wikilinks.py index b535d9c..603682f 100644 --- a/markdown/extensions/wikilinks.py +++ b/markdown/extensions/wikilinks.py @@ -48,7 +48,7 @@ class WikiLinkExtension(Extension): WIKILINK_RE = r'\[\[([\w0-9_ -]+)\]\]' wikilinkPattern = WikiLinksInlineProcessor(WIKILINK_RE, self.getConfigs()) wikilinkPattern.md = md - md.inlinePatterns.add('wikilink', wikilinkPattern, "<not_strong") + md.inlinePatterns.register(wikilinkPattern, 'wikilink', 75) class WikiLinksInlineProcessor(InlineProcessor): diff --git a/markdown/inlinepatterns.py b/markdown/inlinepatterns.py index d67ef1f..343fdd6 100644 --- a/markdown/inlinepatterns.py +++ b/markdown/inlinepatterns.py @@ -44,7 +44,6 @@ So, we apply the expressions in the following order: from __future__ import absolute_import from __future__ import unicode_literals from . import util -from . import odict import re try: # pragma: no cover from html import entities @@ -54,32 +53,32 @@ except ImportError: # pragma: no cover def build_inlinepatterns(md_instance, **kwargs): """ Build the default set of inline patterns for Markdown. """ - inlinePatterns = odict.OrderedDict() - inlinePatterns["backtick"] = BacktickInlineProcessor(BACKTICK_RE) - inlinePatterns["escape"] = EscapeInlineProcessor(ESCAPE_RE, md_instance) - inlinePatterns["reference"] = ReferenceInlineProcessor(REFERENCE_RE, md_instance) - inlinePatterns["link"] = LinkInlineProcessor(LINK_RE, md_instance) - inlinePatterns["image_link"] = ImageInlineProcessor(IMAGE_LINK_RE, md_instance) - inlinePatterns["image_reference"] = ImageReferenceInlineProcessor( - IMAGE_REFERENCE_RE, md_instance + inlinePatterns = util.Registry() + inlinePatterns.register(BacktickInlineProcessor(BACKTICK_RE), 'backtick', 190) + inlinePatterns.register(EscapeInlineProcessor(ESCAPE_RE, md_instance), 'escape', 180) + inlinePatterns.register(ReferenceInlineProcessor(REFERENCE_RE, md_instance), 'reference', 170) + inlinePatterns.register(LinkInlineProcessor(LINK_RE, md_instance), 'link', 160) + inlinePatterns.register(ImageInlineProcessor(IMAGE_LINK_RE, md_instance), 'image_link', 150) + inlinePatterns.register( + ImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md_instance), 'image_reference', 140 ) - inlinePatterns["short_reference"] = ShortReferenceInlineProcessor( - REFERENCE_RE, md_instance + inlinePatterns.register( + ShortReferenceInlineProcessor(REFERENCE_RE, md_instance), 'short_reference', 130 ) - inlinePatterns["autolink"] = AutolinkInlineProcessor(AUTOLINK_RE, md_instance) - inlinePatterns["automail"] = AutomailInlineProcessor(AUTOMAIL_RE, md_instance) - inlinePatterns["linebreak"] = SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br') - inlinePatterns["html"] = HtmlInlineProcessor(HTML_RE, md_instance) - inlinePatterns["entity"] = HtmlInlineProcessor(ENTITY_RE, md_instance) - inlinePatterns["not_strong"] = SimpleTextInlineProcessor(NOT_STRONG_RE) - inlinePatterns["em_strong"] = DoubleTagInlineProcessor(EM_STRONG_RE, 'strong,em') - inlinePatterns["strong_em"] = DoubleTagInlineProcessor(STRONG_EM_RE, 'em,strong') - inlinePatterns["strong"] = SimpleTagInlineProcessor(STRONG_RE, 'strong') - inlinePatterns["emphasis"] = SimpleTagInlineProcessor(EMPHASIS_RE, 'em') + inlinePatterns.register(AutolinkInlineProcessor(AUTOLINK_RE, md_instance), 'autolink', 120) + inlinePatterns.register(AutomailInlineProcessor(AUTOMAIL_RE, md_instance), 'automail', 110) + inlinePatterns.register(SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br'), 'linebreak', 100) + inlinePatterns.register(HtmlInlineProcessor(HTML_RE, md_instance), 'html', 90) + inlinePatterns.register(HtmlInlineProcessor(ENTITY_RE, md_instance), 'entity', 80) + inlinePatterns.register(SimpleTextInlineProcessor(NOT_STRONG_RE), 'not_strong', 70) + inlinePatterns.register(DoubleTagInlineProcessor(EM_STRONG_RE, 'strong,em'), 'em_strong', 60) + inlinePatterns.register(DoubleTagInlineProcessor(STRONG_EM_RE, 'em,strong'), 'strong_em', 50) + inlinePatterns.register(SimpleTagInlineProcessor(STRONG_RE, 'strong'), 'strong', 40) + inlinePatterns.register(SimpleTagInlineProcessor(EMPHASIS_RE, 'em'), 'emphasis', 30) if md_instance.smart_emphasis: - inlinePatterns["emphasis2"] = SimpleTagInlineProcessor(SMART_EMPHASIS_RE, 'em') + inlinePatterns.register(SimpleTagInlineProcessor(SMART_EMPHASIS_RE, 'em'), 'emphasis2', 20) else: - inlinePatterns["emphasis2"] = SimpleTagInlineProcessor(EMPHASIS_2_RE, 'em') + inlinePatterns.register(SimpleTagInlineProcessor(EMPHASIS_2_RE, 'em'), 'emphasis2', 20) return inlinePatterns diff --git a/markdown/odict.py b/markdown/odict.py deleted file mode 100644 index 584ad7c..0000000 --- a/markdown/odict.py +++ /dev/null @@ -1,191 +0,0 @@ -from __future__ import unicode_literals -from __future__ import absolute_import -from . import util -from copy import deepcopy - - -class OrderedDict(dict): - """ - A dictionary that keeps its keys in the order in which they're inserted. - - Copied from Django's SortedDict with some modifications. - - """ - def __new__(cls, *args, **kwargs): - instance = super(OrderedDict, cls).__new__(cls, *args, **kwargs) - instance.keyOrder = [] - return instance - - def __init__(self, data=None): - if data is None or isinstance(data, dict): - data = data or [] - super(OrderedDict, self).__init__(data) - self.keyOrder = list(data) if data else [] - else: - super(OrderedDict, self).__init__() - super_set = super(OrderedDict, self).__setitem__ - for key, value in data: - # Take the ordering from first key - if key not in self: - self.keyOrder.append(key) - # But override with last value in data (dict() does this) - super_set(key, value) - - def __deepcopy__(self, memo): - return self.__class__([(key, deepcopy(value, memo)) - for key, value in self.items()]) - - def __copy__(self): - # The Python's default copy implementation will alter the state - # of self. The reason for this seems complex but is likely related to - # subclassing dict. - return self.copy() - - def __setitem__(self, key, value): - if key not in self: - self.keyOrder.append(key) - super(OrderedDict, self).__setitem__(key, value) - - def __delitem__(self, key): - super(OrderedDict, self).__delitem__(key) - self.keyOrder.remove(key) - - def __iter__(self): - return iter(self.keyOrder) - - def __reversed__(self): - return reversed(self.keyOrder) - - def pop(self, k, *args): - result = super(OrderedDict, self).pop(k, *args) - try: - self.keyOrder.remove(k) - except ValueError: - # Key wasn't in the dictionary in the first place. No problem. - pass - return result - - def popitem(self): - result = super(OrderedDict, self).popitem() - self.keyOrder.remove(result[0]) - return result - - def _iteritems(self): - for key in self.keyOrder: - yield key, self[key] - - def _iterkeys(self): - for key in self.keyOrder: - yield key - - def _itervalues(self): - for key in self.keyOrder: - yield self[key] - - if util.PY3: # pragma: no cover - items = _iteritems - keys = _iterkeys - values = _itervalues - else: # pragma: no cover - iteritems = _iteritems - iterkeys = _iterkeys - itervalues = _itervalues - - def items(self): - return [(k, self[k]) for k in self.keyOrder] - - def keys(self): - return self.keyOrder[:] - - def values(self): - return [self[k] for k in self.keyOrder] - - def update(self, dict_): - for k in dict_: - self[k] = dict_[k] - - def setdefault(self, key, default): - if key not in self: - self.keyOrder.append(key) - return super(OrderedDict, self).setdefault(key, default) - - def value_for_index(self, index): - """Returns the value of the item at the given zero-based index.""" - return self[self.keyOrder[index]] - - def insert(self, index, key, value): - """Inserts the key, value pair before the item with the given index.""" - if key in self.keyOrder: - n = self.keyOrder.index(key) - del self.keyOrder[n] - if n < index: - index -= 1 - self.keyOrder.insert(index, key) - super(OrderedDict, self).__setitem__(key, value) - - def copy(self): - """Returns a copy of this object.""" - # This way of initializing the copy means it works for subclasses, too. - return self.__class__(self) - - def __repr__(self): - """ - Replaces the normal dict.__repr__ with a version that returns the keys - in their Ordered order. - """ - return '{%s}' % ', '.join( - ['%r: %r' % (k, v) for k, v in self._iteritems()] - ) - - def clear(self): - super(OrderedDict, self).clear() - self.keyOrder = [] - - def index(self, key): - """ Return the index of a given key. """ - try: - return self.keyOrder.index(key) - except ValueError: - raise ValueError("Element '%s' was not found in OrderedDict" % key) - - def index_for_location(self, location): - """ Return index or None for a given location. """ - if location == '_begin': - i = 0 - elif location == '_end': - i = None - elif location.startswith('<') or location.startswith('>'): - i = self.index(location[1:]) - if location.startswith('>'): - if i >= len(self): - # last item - i = None - else: - i += 1 - else: - raise ValueError('Not a valid location: "%s". Location key ' - 'must start with a ">" or "<".' % location) - return i - - def add(self, key, value, location): - """ Insert by key location. """ - i = self.index_for_location(location) - if i is not None: - self.insert(i, key, value) - else: - self.__setitem__(key, value) - - def link(self, key, location): - """ Change location of an existing item. """ - n = self.keyOrder.index(key) - del self.keyOrder[n] - try: - i = self.index_for_location(location) - if i is not None: - self.keyOrder.insert(i, key) - else: - self.keyOrder.append(key) - except Exception as e: - # restore to prevent data loss and reraise - self.keyOrder.insert(n, key) - raise e diff --git a/markdown/postprocessors.py b/markdown/postprocessors.py index f59e070..0fb4406 100644 --- a/markdown/postprocessors.py +++ b/markdown/postprocessors.py @@ -12,16 +12,15 @@ from __future__ import absolute_import from __future__ import unicode_literals from collections import OrderedDict from . import util -from . import odict import re def build_postprocessors(md_instance, **kwargs): """ Build the default postprocessors for Markdown. """ - postprocessors = odict.OrderedDict() - postprocessors["raw_html"] = RawHtmlPostprocessor(md_instance) - postprocessors["amp_substitute"] = AndSubstitutePostprocessor() - postprocessors["unescape"] = UnescapePostprocessor() + postprocessors = util.Registry() + postprocessors.register(RawHtmlPostprocessor(md_instance), 'raw_html', 30) + postprocessors.register(AndSubstitutePostprocessor(), 'amp_substitute', 20) + postprocessors.register(UnescapePostprocessor(), 'unescape', 10) return postprocessors diff --git a/markdown/preprocessors.py b/markdown/preprocessors.py index 8c87ecf..cac0037 100644 --- a/markdown/preprocessors.py +++ b/markdown/preprocessors.py @@ -9,16 +9,15 @@ complicated. from __future__ import absolute_import from __future__ import unicode_literals from . import util -from . import odict import re def build_preprocessors(md_instance, **kwargs): """ Build the default set of preprocessors used by Markdown. """ - preprocessors = odict.OrderedDict() - preprocessors['normalize_whitespace'] = NormalizeWhitespace(md_instance) - preprocessors["html_block"] = HtmlBlockPreprocessor(md_instance) - preprocessors["reference"] = ReferencePreprocessor(md_instance) + preprocessors = util.Registry() + preprocessors.register(NormalizeWhitespace(md_instance), 'normalize_whitespace', 30) + preprocessors.register(HtmlBlockPreprocessor(md_instance), 'html_block', 20) + preprocessors.register(ReferencePreprocessor(md_instance), 'reference', 10) return preprocessors diff --git a/markdown/treeprocessors.py b/markdown/treeprocessors.py index df5e748..0177e43 100644 --- a/markdown/treeprocessors.py +++ b/markdown/treeprocessors.py @@ -1,15 +1,14 @@ from __future__ import unicode_literals from __future__ import absolute_import from . import util -from . import odict from . import inlinepatterns def build_treeprocessors(md_instance, **kwargs): """ Build the default treeprocessors for Markdown. """ - treeprocessors = odict.OrderedDict() - treeprocessors["inline"] = InlineProcessor(md_instance) - treeprocessors["prettify"] = PrettifyTreeprocessor(md_instance) + treeprocessors = util.Registry() + treeprocessors.register(InlineProcessor(md_instance), 'inline', 20) + treeprocessors.register(PrettifyTreeprocessor(md_instance), 'prettify', 10) return treeprocessors @@ -103,8 +102,8 @@ class InlineProcessor(Treeprocessor): startIndex = 0 while patternIndex < len(self.inlinePatterns): data, matched, startIndex = self.__applyPattern( - self.inlinePatterns.value_for_index(patternIndex), - data, patternIndex, startIndex) + self.inlinePatterns[patternIndex], data, patternIndex, startIndex + ) if not matched: patternIndex += 1 return data diff --git a/markdown/util.py b/markdown/util.py index 6591cf5..697cf45 100644 --- a/markdown/util.py +++ b/markdown/util.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals import re import sys +from collections import namedtuple +import warnings """ @@ -179,3 +181,216 @@ class HtmlStash(object): placeholder = TAG_PLACEHOLDER % str(self.tag_counter) self.tag_counter += 1 # equal to the tag's index in self.tag_data return placeholder + + +# Used internally by `Registry` for each item in its sorted list. +# Provides an easier to read API when editing the code later. +# For example, `item.name` is more clear than `item[0]`. +_PriorityItem = namedtuple('PriorityItem', ['name', 'priority']) + + +class Registry(object): + """ + A priority sorted registry. + + A `Registry` instance provides two public methods to alter the data of the + registry: `register` and `deregister`. Use `register` to add items and + `deregister` to remove items. See each method for specifics. + + When registering an item, a "name" and a "priority" must be provided. All + items are automatically sorted by "priority" from highest to lowest. The + "name" is used to remove ("deregister") and get items. + + A `Registry` instance it like a list (which maintains order) when reading + data. You may iterate over the items, get an item and get a count (length) + of all items. You may also check that the registry contains an item. + + When getting an item you may use either the index of the item or the + string-based "name". For example: + + registry = Registry() + registry.register(SomeItem(), 'itemname', 20) + # Get the item by index + item = registry[0] + # Get the item by name + item = registry['itemname'] + + When checking that the registry contains an item, you may use either the + string-based "name", or a reference to the actual item. For example: + + someitem = SomeItem() + registry.register(someitem, 'itemname', 20) + # Contains the name + assert 'itemname' in registry + # Contains the item instance + assert someitem in registry + + The method `get_index_for_name` is also available to obtain the index of + an item using that item's assigned "name". + """ + + def __init__(self): + self._data = {} + self._priority = [] + self._is_sorted = False + + def __contains__(self, item): + if isinstance(item, string_type): + # Check if an item exists by this name. + return item in self._data.keys() + # Check if this instance exists. + return item in self._data.values() + + def __iter__(self): + self._sort() + return iter([self._data[k] for k, p in self._priority]) + + def __getitem__(self, key): + self._sort() + if isinstance(key, slice): + data = Registry() + for k, p in self._priority[key]: + data.register(self._data[k], k, p) + return data + if isinstance(key, int): + return self._data[self._priority[key].name] + return self._data[key] + + def __len__(self): + return len(self._priority) + + def __repr__(self): + return '<{0}({1})>'.format(self.__class__.__name__, list(self)) + + def get_index_for_name(self, name): + """ + Return the index of the given name. + """ + if name in self: + self._sort() + return self._priority.index( + [x for x in self._priority if x.name == name][0] + ) + raise ValueError('No item named "{0}" exists.'.format(name)) + + def register(self, item, name, priority): + """ + Add an item to the registry with the given name and priority. + + Parameters: + + * `item`: The item being registered. + * `name`: A string used to reference the item. + * `priority`: An integer or float used to sort against all items. + + If an item is registered with a "name" which already exists, the + existing item is replaced with the new item. Tread carefully as the + old item is lost with no way to recover it. The new item will be + sorted according to its priority and will **not** retain the position + of the old item. + """ + if name in self: + # Remove existing item of same name first + self.deregister(name) + self._is_sorted = False + self._data[name] = item + self._priority.append(_PriorityItem(name, priority)) + + def deregister(self, name, strict=True): + """ + Remove an item from the registry. + + Set `strict=False` to fail silently. + """ + try: + index = self.get_index_for_name(name) + del self._priority[index] + del self._data[name] + except ValueError: + if strict: + raise + + def _sort(self): + """ + Sort the registry by priority from highest to lowest. + + This method is called internally and should never be explicitly called. + """ + if not self._is_sorted: + self._priority.sort(key=lambda item: item.priority, reverse=True) + self._is_sorted = True + + # Deprecated Methods which provide a smooth transition from OrderedDict + + def __setitem__(self, key, value): + """ Register item with priorty 5 less than lowest existing priority. """ + if isinstance(key, string_type): + warnings.warn( + 'Using setitem to register a processor or pattern is deprecated. ' + 'Use the `register` method instead.', DeprecationWarning + ) + if key in self: + # Key already exists, replace without altering priority + self._data[key] = value + return + if len(self) == 0: + # This is the first item. Set priority to 50. + priority = 50 + else: + self._sort() + priority = self._priority[-1].priority - 5 + self.register(value, key, priority) + else: + raise TypeError + + def __delitem__(self, key): + """ Deregister an item by name. """ + if key in self: + self.deregister(key) + warnings.warn( + 'Using del to remove a processor or pattern is deprecated. ' + 'Use the `deregister` method instead.', DeprecationWarning + ) + else: + raise TypeError + + def add(self, key, value, location): + """ Register a key by location. """ + if len(self) == 0: + # This is the first item. Set priority to 50. + priority = 50 + elif location == '_begin': + self._sort() + # Set priority 5 greater than highest existing priority + priority = self._priority[0].priority + 5 + elif location == '_end': + self._sort() + # Set priority 5 less than lowest existing priority + priority = self._priority[-1].priority - 5 + elif location.startswith('<') or location.startswith('>'): + # Set priority halfway between existing priorities. + i = self.get_index_for_name(location[1:]) + if location.startswith('<'): + after = self._priority[i].priority + if i > 0: + before = self._priority[i-1].priority + else: + # Location is first item` + before = after + 10 + else: + # location.startswith('>') + before = self._priority[i].priority + if i < len(self) - 1: + after = self._priority[i+1].priority + else: + # location is last item + after = before - 10 + priority = before - ((before - after) / 2) + else: + raise ValueError('Not a valid location: "%s". Location key ' + 'must start with a ">" or "<".' % location) + self.register(value, key, priority) + warnings.warn( + 'Using the add method to register a processor or pattern is deprecated. ' + 'Use the `register` method instead.', DeprecationWarning + ) diff --git a/tests/test_apis.py b/tests/test_apis.py index 2875c85..3941bd4 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -202,152 +202,217 @@ class TestHtmlStash(unittest.TestCase): self.assertEqual(self.stash.rawHtmlBlocks, []) -class TestOrderedDict(unittest.TestCase): - """ Test OrderedDict storage class. """ - - def setUp(self): - self.odict = markdown.odict.OrderedDict() - self.odict['first'] = 'This' - self.odict['third'] = 'a' - self.odict['fourth'] = 'self' - self.odict['fifth'] = 'test' - - def testValues(self): - """ Test output of OrderedDict.values(). """ - self.assertEqual(list(self.odict.values()), ['This', 'a', 'self', 'test']) - - def testKeys(self): - """ Test output of OrderedDict.keys(). """ - self.assertEqual( - list(self.odict.keys()), - ['first', 'third', 'fourth', 'fifth'] - ) - - def testItems(self): - """ Test output of OrderedDict.items(). """ - self.assertEqual( - list(self.odict.items()), [ - ('first', 'This'), - ('third', 'a'), - ('fourth', 'self'), - ('fifth', 'test') - ] - ) - - def testAddBefore(self): - """ Test adding an OrderedDict item before a given key. """ - self.odict.add('second', 'is', '<third') - self.assertEqual( - list(self.odict.items()), [ - ('first', 'This'), - ('second', 'is'), - ('third', 'a'), - ('fourth', 'self'), - ('fifth', 'test') - ] - ) - - def testAddAfter(self): - """ Test adding an OrderDict item after a given key. """ - self.odict.add('second', 'is', '>first') - self.assertEqual( - list(self.odict.items()), [ - ('first', 'This'), - ('second', 'is'), - ('third', 'a'), - ('fourth', 'self'), - ('fifth', 'test') - ] - ) - - def testAddAfterEnd(self): - """ Test adding an OrderedDict item after the last key. """ - self.odict.add('sixth', '.', '>fifth') - self.assertEqual( - list(self.odict.items()), [ - ('first', 'This'), - ('third', 'a'), - ('fourth', 'self'), - ('fifth', 'test'), - ('sixth', '.') - ] - ) - - def testAdd_begin(self): - """ Test adding an OrderedDict item using "_begin". """ - self.odict.add('zero', 'CRAZY', '_begin') - self.assertEqual( - list(self.odict.items()), [ - ('zero', 'CRAZY'), - ('first', 'This'), - ('third', 'a'), - ('fourth', 'self'), - ('fifth', 'test') - ] - ) - - def testAdd_end(self): - """ Test adding an OrderedDict item using "_end". """ - self.odict.add('sixth', '.', '_end') - self.assertEqual( - list(self.odict.items()), [ - ('first', 'This'), - ('third', 'a'), - ('fourth', 'self'), - ('fifth', 'test'), - ('sixth', '.') - ] - ) - - def testAddBadLocation(self): - """ Test Error on bad location in OrderedDict.add(). """ - self.assertRaises(ValueError, self.odict.add, 'sixth', '.', '<seventh') - self.assertRaises(ValueError, self.odict.add, 'second', 'is', 'third') - - def testDeleteItem(self): - """ Test deletion of an OrderedDict item. """ - del self.odict['fourth'] - self.assertEqual( - list(self.odict.items()), - [('first', 'This'), ('third', 'a'), ('fifth', 'test')] - ) - - def testChangeValue(self): - """ Test OrderedDict change value. """ - self.odict['fourth'] = 'CRAZY' - self.assertEqual( - list(self.odict.items()), [ - ('first', 'This'), - ('third', 'a'), - ('fourth', 'CRAZY'), - ('fifth', 'test') - ] - ) - - def testChangeOrder(self): - """ Test OrderedDict change order. """ - self.odict.link('fourth', '<third') - self.assertEqual( - list(self.odict.items()), [ - ('first', 'This'), - ('fourth', 'self'), - ('third', 'a'), - ('fifth', 'test') - ] - ) - - def textBadLink(self): - """ Test OrderedDict change order with bad location. """ - self.assertRaises(ValueError, self.odict.link('fourth', '<bad')) - # Check for data integrity ("fourth" wasn't deleted).' - self.assertEqual( - list(self.odict.items()), [ - ('first', 'This'), - ('third', 'a'), - ('fourth', 'self'), - ('fifth', 'test') - ] - ) +class Item(object): + """ A dummy Registry item object for testing. """ + def __init__(self, data): + self.data = data + + def __repr__(self): + return repr(self.data) + + def __eq__(self, other): + return self.data == other + + +class RegistryTests(unittest.TestCase): + """ Test the processor registry. """ + + def testCreateRegistry(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + self.assertEqual(len(r), 1) + self.assertTrue(isinstance(r, markdown.util.Registry)) + + def testRegisterWithoutPriority(self): + r = markdown.util.Registry() + with self.assertRaises(TypeError): + r.register(Item('a')) + + def testSortRegistry(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + r.register(Item('b'), 'b', 21) + r.register(Item('c'), 'c', 20.5) + self.assertEqual(len(r), 3) + self.assertEqual(list(r), ['b', 'c', 'a']) + + def testIsSorted(self): + r = markdown.util.Registry() + self.assertFalse(r._is_sorted) + r.register(Item('a'), 'a', 20) + list(r) + self.assertTrue(r._is_sorted) + r.register(Item('b'), 'b', 21) + self.assertFalse(r._is_sorted) + r['a'] + self.assertTrue(r._is_sorted) + r._is_sorted = False + r.get_index_for_name('a') + self.assertTrue(r._is_sorted) + r._is_sorted = False + repr(r) + self.assertTrue(r._is_sorted) + + def testDeregister(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + r.register(Item('b'), 'b', 30) + r.register(Item('c'), 'c', 40) + self.assertEqual(len(r), 3) + r.deregister('b') + self.assertEqual(len(r), 2) + r.deregister('c', strict=False) + self.assertEqual(len(r), 1) + # deregister non-existant item with strict=False + r.deregister('d', strict=False) + self.assertEqual(len(r), 1) + with self.assertRaises(ValueError): + # deregister non-existant item with strict=True + r.deregister('e') + self.assertEqual(list(r), ['a']) + + def testRegistryContains(self): + r = markdown.util.Registry() + item = Item('a') + r.register(item, 'a', 20) + self.assertTrue('a' in r) + self.assertTrue(item in r) + self.assertFalse('b' in r) + + def testRegistryIter(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + r.register(Item('b'), 'b', 30) + self.assertEqual(list(r), ['b', 'a']) + + def testRegistryGetItemByIndex(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + r.register(Item('b'), 'b', 30) + self.assertEqual(r[0], 'b') + self.assertEqual(r[1], 'a') + with self.assertRaises(IndexError): + r[3] + + def testRegistryGetItemByItem(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + r.register(Item('b'), 'b', 30) + self.assertEqual(r['a'], 'a') + self.assertEqual(r['b'], 'b') + with self.assertRaises(KeyError): + r['c'] + + def testRegistrySetItem(self): + r = markdown.util.Registry() + with self.assertRaises(TypeError): + r[0] = 'a' + # TODO: restore this when deprecated __setitem__ is removed. + # with self.assertRaises(TypeError): + # r['a'] = 'a' + # TODO: remove this when deprecated __setitem__ is removed. + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + r['a'] = Item('a') + self.assertEqual(list(r), ['a']) + r['b'] = Item('b') + self.assertEqual(list(r), ['a', 'b']) + r['a'] = Item('a1') + self.assertEqual(list(r), ['a1', 'b']) + + # Check the warnings + self.assertEqual(len(w), 3) + self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w)) + + def testRegistryDelItem(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + with self.assertRaises(TypeError): + del r[0] + # TODO: restore this when deprecated __del__ is removed. + # with self.assertRaises(TypeError): + # del r['a'] + # TODO: remove this when deprecated __del__ is removed. + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + r.register(Item('b'), 'b', 15) + r.register(Item('c'), 'c', 10) + del r['b'] + self.assertEqual(list(r), ['a', 'c']) + del r['a'] + self.assertEqual(list(r), ['c']) + with self.assertRaises(TypeError): + del r['badname'] + del r['c'] + self.assertEqual(list(r), []) + + # Check the warnings + self.assertEqual(len(w), 3) + self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w)) + + def testRegistrySlice(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + r.register(Item('b'), 'b', 30) + r.register(Item('c'), 'c', 40) + slc = r[1:] + self.assertEqual(len(slc), 2) + self.assertTrue(isinstance(slc, markdown.util.Registry)) + self.assertEqual(list(slc), ['b', 'a']) + + def testGetIndexForName(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + r.register(Item('b'), 'b', 30) + self.assertEqual(r.get_index_for_name('a'), 1) + self.assertEqual(r.get_index_for_name('b'), 0) + with self.assertRaises(ValueError): + r.get_index_for_name('c') + + def testRegisterDupplicate(self): + r = markdown.util.Registry() + r.register(Item('a'), 'a', 20) + r.register(Item('b1'), 'b', 10) + self.assertEqual(list(r), ['a', 'b1']) + self.assertEqual(len(r), 2) + r.register(Item('b2'), 'b', 30) + self.assertEqual(len(r), 2) + self.assertEqual(list(r), ['b2', 'a']) + + def testRegistryDeprecatedAdd(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + r = markdown.util.Registry() + # Add first item + r.add('c', Item('c'), '_begin') + self.assertEqual(list(r), ['c']) + # Added to beginning + r.add('b', Item('b'), '_begin') + self.assertEqual(list(r), ['b', 'c']) + # Add before first item + r.add('a', Item('a'), '<b') + self.assertEqual(list(r), ['a', 'b', 'c']) + # Add before non-first item + r.add('a1', Item('a1'), '<b') + self.assertEqual(list(r), ['a', 'a1', 'b', 'c']) + # Add after non-last item + r.add('b1', Item('b1'), '>b') + self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c']) + # Add after last item + r.add('d', Item('d'), '>c') + self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c', 'd']) + # Add to end + r.add('e', Item('e'), '_end') + self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c', 'd', 'e']) + with self.assertRaises(ValueError): + r.add('f', Item('f'), 'badlocation') + + # Check the warnings + self.assertEqual(len(w), 7) + self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w)) class TestErrors(unittest.TestCase): @@ -862,7 +927,7 @@ class TestAncestorExclusion(unittest.TestCase): """Modify inline patterns.""" pattern = r'(\+)([^\+]+)\1' - md.inlinePatterns["ancestor-test"] = TestAncestorExclusion.AncestorExample(pattern, 'strong') + md.inlinePatterns.register(TestAncestorExclusion.AncestorExample(pattern, 'strong'), 'ancestor-test', 0) def setUp(self): """Setup markdown object.""" |