~netlandish/django-wiki

6c801b2b271e16fbd17dcd0b4be857ea9c79713a — Mathias Rav 6 years ago dd09cab
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.
M src/wiki/plugins/images/markdown_extensions.py => src/wiki/plugins/images/markdown_extensions.py +54 -76
@@ 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<id>[0-9]+)(\s+align\:(?P<align>right|left))?(\s+size\:(?P<size>default|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<id>[0-9]+)" +
    # Match optional 'align'
    r"(?:\s+align\:(?P<align>right|left))?" +
    # Match optional 'size'
    r"(?:\s+size\:(?P<size>default|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<caption>(?:\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):

M src/wiki/plugins/macros/mdx/macro.py => src/wiki/plugins/macros/mdx/macro.py +27 -39
@@ 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<macro>\w+)(?P<kwargs>\s\w+\:.+)*\]).*',
    re.IGNORECASE)
MACRO_RE = r"((?i)\[(?P<macro>\w+)(?P<kwargs>\s\w+\:.+)*\])"
KWARG_RE = re.compile(
    r'\s*(?P<arg>\w+)(:(?P<value>([^\']+|%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(

M src/wiki/plugins/macros/templatetags/wiki_macro_tags.py => src/wiki/plugins/macros/templatetags/wiki_macro_tags.py +2 -2
@@ 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

A tests/plugins/images/test_markdown.py => tests/plugins/images/test_markdown.py +16 -0
@@ 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("<figure", md_text)
        self.assertNotIn("[image:1 align:left]", md_text)

    def test_escape(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("`[image:1 align:left]`")
        self.assertNotIn("<figure", md_text)
        self.assertIn("[image:1 align:left]", md_text)

A tests/plugins/macros/test_macro.py => tests/plugins/macros/test_macro.py +16 -0
@@ 0,0 1,16 @@
from tests.base import RequireRootArticleMixin, TestBase
from wiki.core import markdown


class MacroTests(RequireRootArticleMixin, TestBase):
    def test_article_list(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("[article_list depth:2]")
        self.assertIn("Nothing below this level", md_text)
        self.assertNotIn("[article_list depth:2]", md_text)

    def test_escape(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("`[article_list depth:2]`")
        self.assertNotIn("Nothing below this level", md_text)
        self.assertIn("[article_list depth:2]", md_text)