From 6c801b2b271e16fbd17dcd0b4be857ea9c79713a Mon Sep 17 00:00:00 2001 From: Mathias Rav Date: Fri, 3 Aug 2018 12:24:55 +0200 Subject: [PATCH] Allow escaping [image] and [article_list] Change ImagePreprocessor and MacroPreprocessor into inline processors. Preprocessors run before processing of backticks, which means that the two tags would be handled by the images and macros plugins before the backtick processor would escape the contents. Add tests to ensure that escaping [image] and [article_list] works as expected. --- .../plugins/images/markdown_extensions.py | 130 ++++++++---------- src/wiki/plugins/macros/mdx/macro.py | 66 ++++----- .../macros/templatetags/wiki_macro_tags.py | 4 +- tests/plugins/images/test_markdown.py | 16 +++ tests/plugins/macros/test_macro.py | 16 +++ 5 files changed, 115 insertions(+), 117 deletions(-) create mode 100644 tests/plugins/images/test_markdown.py create mode 100644 tests/plugins/macros/test_macro.py diff --git a/src/wiki/plugins/images/markdown_extensions.py b/src/wiki/plugins/images/markdown_extensions.py index 35f915a1..cd074a04 100644 --- a/src/wiki/plugins/images/markdown_extensions.py +++ b/src/wiki/plugins/images/markdown_extensions.py @@ -1,12 +1,23 @@ -import re - import markdown from django.template.loader import render_to_string from wiki.plugins.images import models, settings -IMAGE_RE = re.compile( - r'.*(\[image\:(?P[0-9]+)(\s+align\:(?Pright|left))?(\s+size\:(?Pdefault|small|medium|large|orig))?\s*\]).*', - re.IGNORECASE) +IMAGE_RE = ( + # Match only at the beginning of a line + r"(?:(?im)^\s*" + + # Match '[image:N' + r"\[image\:(?P[0-9]+)" + + # Match optional 'align' + r"(?:\s+align\:(?Pright|left))?" + + # Match optional 'size' + r"(?:\s+size\:(?Pdefault|small|medium|large|orig))?" + + # Match ']' at end of line + r"\s*\]\s*$" + + # Match zero or more caption lines, each indented by four spaces. + # Normally [^\n] could be replaced with a dot '.', since '.' + # does not match newlines, but inline processors run with re.DOTALL. + r"(?P(?:\n [^\n]*)*))" +) class ImageExtension(markdown.Extension): @@ -14,93 +25,60 @@ class ImageExtension(markdown.Extension): """ Images plugin markdown extension for django-wiki. """ def extendMarkdown(self, md, md_globals): - """ Insert ImagePreprocessor before ReferencePreprocessor. """ - md.preprocessors.add('dw-images', ImagePreprocessor(md), '>html_block') + md.inlinePatterns.add('dw-images', ImagePattern(IMAGE_RE, md), '>link') md.postprocessors.add('dw-images-cleanup', ImagePostprocessor(md), '>raw_html') -class ImagePreprocessor(markdown.preprocessors.Preprocessor): +class ImagePattern(markdown.inlinepatterns.Pattern): """ django-wiki image preprocessor - Parse text for [image:id align:left|right|center] references. + Parse text for [image:N align:ALIGN size:SIZE] references. For instance: - [image:id align:left|right|center] + [image:id align:left|right] This is the caption text maybe with [a link](...) So: Remember that the caption text is fully valid markdown! """ - def run(self, lines): # NOQA - new_text = [] - previous_line = "" - line_index = None - previous_line_was_image = False + def handleMatch(self, m): image = None image_id = None alignment = None - size = settings.THUMBNAIL_SIZES['default'] - caption_lines = [] - for line in lines: - m = IMAGE_RE.match(line) - if m: - previous_line_was_image = True - image_id = m.group('id').strip() - alignment = m.group('align') - if m.group('size'): - size = settings.THUMBNAIL_SIZES[m.group('size')] - try: - image = models.Image.objects.get( - article=self.markdown.article, - id=image_id, - current_revision__deleted=False) - except models.Image.DoesNotExist: - pass - line_index = line.find(m.group(1)) - line = line.replace(m.group(1), "") - previous_line = line - caption_lines = [] - elif previous_line_was_image: - if line.startswith(" "): - caption_lines.append(line[4:]) - line = None - else: - caption_placeholder = "{{{IMAGECAPTION}}}" - width = size.split("x")[0] if size else None - html = render_to_string( - "wiki/plugins/images/render.html", - context={ - 'image': image, - 'caption': caption_placeholder, - 'align': alignment, - 'size': size, - 'width': width - }) - html_before, html_after = html.split(caption_placeholder) - placeholder_before = self.markdown.htmlStash.store( - html_before, - safe=True) - placeholder_after = self.markdown.htmlStash.store( - html_after, - safe=True) - new_line = placeholder_before + "\n".join( - caption_lines) + placeholder_after + "\n" - previous_line_was_image = False - if previous_line is not "": - if previous_line[line_index:] is not "": - new_line = new_line[0:-1] - new_text[-1] = (previous_line[0:line_index] + - new_line + - previous_line[line_index:] + - "\n" + - line) - line = None - else: - line = new_line + line - if line is not None: - new_text.append(line) - return new_text + size = settings.THUMBNAIL_SIZES["default"] + + image_id = m.group("id").strip() + alignment = m.group("align") + if m.group("size"): + size = settings.THUMBNAIL_SIZES[m.group("size")] + try: + image = models.Image.objects.get( + article=self.markdown.article, + id=image_id, + current_revision__deleted=False, + ) + except models.Image.DoesNotExist: + pass + + caption = m.group("caption") + + caption_placeholder = "{{{IMAGECAPTION}}}" + width = size.split("x")[0] if size else None + html = render_to_string( + "wiki/plugins/images/render.html", + context={ + "image": image, + "caption": caption_placeholder, + "align": alignment, + "size": size, + "width": width, + }, + ) + html_before, html_after = html.split(caption_placeholder) + placeholder_before = self.markdown.htmlStash.store(html_before, safe=True) + placeholder_after = self.markdown.htmlStash.store(html_after, safe=True) + return placeholder_before + caption + placeholder_after class ImagePostprocessor(markdown.postprocessors.Postprocessor): diff --git a/src/wiki/plugins/macros/mdx/macro.py b/src/wiki/plugins/macros/mdx/macro.py index e5adb630..87e0c981 100644 --- a/src/wiki/plugins/macros/mdx/macro.py +++ b/src/wiki/plugins/macros/mdx/macro.py @@ -9,9 +9,7 @@ from wiki.plugins.macros import settings # http://stackoverflow.com/questions/430759/regex-for-managing-escaped-characters-for-items-like-string-literals re_sq_short = r"'([^'\\]*(?:\\.[^'\\]*)*)'" -MACRO_RE = re.compile( - r'.*(\[(?P\w+)(?P\s\w+\:.+)*\]).*', - re.IGNORECASE) +MACRO_RE = r"((?i)\[(?P\w+)(?P\s\w+\:.+)*\])" KWARG_RE = re.compile( r'\s*(?P\w+)(:(?P([^\']+|%s)))?' % re_sq_short, @@ -23,48 +21,38 @@ class MacroExtension(markdown.Extension): """ Macro plugin markdown extension for django-wiki. """ def extendMarkdown(self, md, md_globals): - """ Insert MacroPreprocessor before ReferencePreprocessor. """ - md.preprocessors.add('dw-macros', MacroPreprocessor(md), '>html_block') + md.inlinePatterns.add('dw-macros', MacroPattern(MACRO_RE, md), '>link') -class MacroPreprocessor(markdown.preprocessors.Preprocessor): +class MacroPattern(markdown.inlinepatterns.Pattern): """django-wiki macro preprocessor - parse text for various [some_macro] and [some_macro (kw:arg)*] references. """ - def run(self, lines): - # Look at all those indentations. - # That's insane, let's get a helper library - # Please note that this pattern is also in plugins.images - new_text = [] - for line in lines: - m = MACRO_RE.match(line) - if m: - macro = m.group('macro').strip() - if macro in settings.METHODS and hasattr(self, macro): - kwargs = m.group('kwargs') - if kwargs: - kwargs_dict = {} - for kwarg in KWARG_RE.finditer(kwargs): - arg = kwarg.group('arg') - value = kwarg.group('value') - if value is None: - value = True - if isinstance(value, str): - # If value is enclosed with ': Remove and - # remove escape sequences - if value.startswith("'") and len(value) > 2: - value = value[1:-1] - value = value.replace("\\\\", "¤KEEPME¤") - value = value.replace("\\", "") - value = value.replace("¤KEEPME¤", "\\") - kwargs_dict[str(arg)] = value - line = getattr(self, macro)(**kwargs_dict) - else: - line = getattr(self, macro)() - if line is not None: - new_text.append(line) - return new_text + def handleMatch(self, m): + macro = m.group('macro').strip() + if macro not in settings.METHODS or not hasattr(self, macro): + return m.group(2) + + kwargs = m.group('kwargs') + if not kwargs: + return getattr(self, macro)() + kwargs_dict = {} + for kwarg in KWARG_RE.finditer(kwargs): + arg = kwarg.group('arg') + value = kwarg.group('value') + if value is None: + value = True + if isinstance(value, str): + # If value is enclosed with ': Remove and + # remove escape sequences + if value.startswith("'") and len(value) > 2: + value = value[1:-1] + value = value.replace("\\\\", "¤KEEPME¤") + value = value.replace("\\", "") + value = value.replace("¤KEEPME¤", "\\") + kwargs_dict[str(arg)] = value + return getattr(self, macro)(**kwargs_dict) def article_list(self, depth="2"): html = render_to_string( diff --git a/src/wiki/plugins/macros/templatetags/wiki_macro_tags.py b/src/wiki/plugins/macros/templatetags/wiki_macro_tags.py index e4f611cd..fb02e1fa 100644 --- a/src/wiki/plugins/macros/templatetags/wiki_macro_tags.py +++ b/src/wiki/plugins/macros/templatetags/wiki_macro_tags.py @@ -1,6 +1,6 @@ from django import template from wiki.plugins.macros import settings -from wiki.plugins.macros.mdx.macro import MacroPreprocessor +from wiki.plugins.macros.mdx.macro import MacroPattern register = template.Library() @@ -19,6 +19,6 @@ def article_list(context, urlpath, depth): def allowed_macros(): for method in settings.METHODS: try: - yield getattr(MacroPreprocessor, method).meta + yield getattr(MacroPattern, method).meta except AttributeError: continue diff --git a/tests/plugins/images/test_markdown.py b/tests/plugins/images/test_markdown.py new file mode 100644 index 00000000..b5bc451a --- /dev/null +++ b/tests/plugins/images/test_markdown.py @@ -0,0 +1,16 @@ +from tests.base import RequireRootArticleMixin, TestBase +from wiki.core import markdown + + +class ImageMarkdownTests(RequireRootArticleMixin, TestBase): + def test_markdown(self): + md = markdown.ArticleMarkdown(article=self.root_article) + md_text = md.convert("[image:1 align:left]") + self.assertIn("