diff --git a/markdown/blockparser.py b/markdown/blockparser.py index 32d3254cd..5e9d56756 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 @@ 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 diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index db9add5d5..50c459132 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -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 diff --git a/markdown/core.py b/markdown/core.py index 4b8d1a684..06bf26266 100644 --- a/markdown/core.py +++ b/markdown/core.py @@ -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 @@ -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() diff --git a/markdown/extensions/abbr.py b/markdown/extensions/abbr.py index 5e8845b0b..a3d456fa7 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), '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 fed4d6457..8b9cd8f95 100644 --- a/markdown/extensions/codehilite.py +++ b/markdown/extensions/codehilite.py @@ -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, "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 d1294e0d9..da4cb386e 100644 --- a/markdown/extensions/extra.py +++ b/markdown/extensions/extra.py @@ -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) diff --git a/markdown/extensions/fenced_code.py b/markdown/extensions/fenced_code.py index e1a616ee2..c38dabfea 100644 --- a/markdown/extensions/fenced_code.py +++ b/markdown/extensions/fenced_code.py @@ -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): diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index a957278ba..2b9cc4037 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -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 @@ -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), "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 @@ 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. @@ -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 @@ -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 diff --git a/markdown/extensions/legacy_attrs.py b/markdown/extensions/legacy_attrs.py index b28223f3c..740f9d6e6 100644 --- a/markdown/extensions/legacy_attrs.py +++ b/markdown/extensions/legacy_attrs.py @@ -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 diff --git a/markdown/extensions/meta.py b/markdown/extensions/meta.py index 2c2c8e35f..27adcb259 100644 --- a/markdown/extensions/meta.py +++ b/markdown/extensions/meta.py @@ -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 = {} diff --git a/markdown/extensions/nl2br.py b/markdown/extensions/nl2br.py index 5b9373f62..d334b0282 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 89f929f95..7fb4fd6da 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 f34531d6f..e7a15d98c 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 189651fcf..d25620b2f 100644 --- a/markdown/extensions/smarty.py +++ b/markdown/extensions/smarty.py @@ -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. @@ -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( @@ -195,16 +194,14 @@ def educateDashes(self, md): enDashesPattern = SubstituteTextPattern( r'(?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'(?\>', (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 @@ 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']: @@ -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(['"', "'"]) diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py index b8218b01d..0f221a6ff 100644 --- a/markdown/extensions/tables.py +++ b/markdown/extensions/tables.py @@ -218,9 +218,7 @@ def extendMarkdown(self, md, md_globals): """ 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), - ''): - 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 f59e0700f..0fb4406d1 100644 --- a/markdown/postprocessors.py +++ b/markdown/postprocessors.py @@ -12,16 +12,15 @@ 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 8c87ecfda..cac003714 100644 --- a/markdown/preprocessors.py +++ b/markdown/preprocessors.py @@ -9,16 +9,15 @@ 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 df5e74811..0177e43f1 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 @@ def __handleInline(self, data, patternIndex=0): 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 6591cf50e..697cf45af 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 @@ def store_tag(self, tag, attrs, left_index, right_index): 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 2875c85d9..3941bd48f 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -202,152 +202,217 @@ def testReset(self): 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', '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', '.', '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 @@ def extendMarkdown(self, md, md_globals): """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."""