~netlandish/django-wiki

055dd132ab56b9a08e1bdc51647400c525795a15 — Benjamin Bach 6 years ago 7cebf86 + f3f10c6
Merge pull request #896 from Mortal/macro-inline

Allow escaping [image] and [article_list]
M src/wiki/plugins/images/markdown_extensions.py => src/wiki/plugins/images/markdown_extensions.py +54 -76
@@ 1,12 1,22 @@
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 = (
    r"(?:(?im)" +
    # 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 ']' and rest of line.
    # Normally [^\n] could be replaced with a dot '.', since '.'
    # does not match newlines, but inline processors run with re.DOTALL.
    r"\s*\](?P<trailer>[^\n]*)$" +
    # Match zero or more caption lines, each indented by four spaces.
    r"(?P<caption>(?:\n    [^\n]*)*))"
)


class ImageExtension(markdown.Extension):


@@ 14,93 24,61 @@ 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")
        trailer = m.group('trailer')

        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 + trailer


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 +81 -0
@@ 0,0 1,81 @@
import base64
from io import BytesIO

from django.core.files.uploadedfile import InMemoryUploadedFile
from tests.base import RequireRootArticleMixin, TestBase
from wiki.core import markdown
from wiki.plugins.images import models


class ImageMarkdownTests(RequireRootArticleMixin, TestBase):
    def setUp(self):
        super().setUp()

        self.image_revision = models.ImageRevision(
            image=self._create_test_gif_file(), width=1, height=1
        )
        self.image = models.Image(article=self.root_article)
        self.image.add_revision(self.image_revision)
        self.assertEqual(1, self.image.id)

    def _create_test_gif_file(self):
        # A black 1x1 gif
        str_base64 = "R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="
        filename = "test.gif"
        data = base64.b64decode(str_base64)
        filedata = BytesIO(data)
        return InMemoryUploadedFile(filedata, None, filename, "image", len(data), None)

    def test_before_and_after(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("before [image:%s align:left] after" % self.image.id)
        before_pos = md_text.index("before")
        figure_pos = md_text.index("<figure")
        after_pos = md_text.index("after")
        self.assertTrue(before_pos < figure_pos < after_pos)

    def test_markdown(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("[image:%s align:left]" % self.image.id)
        self.assertIn("<figure", md_text)
        self.assertNotIn("[image:%s align:left]" % self.image.id, md_text)
        md_text = md.convert("image: [image:%s align:left]\nadasd" % self.image.id)
        self.assertIn("<figure", md_text)
        self.assertIn("<figcaption", md_text)
        md_text = md.convert(
            "image: [image:%s align:right size:medium]\nadasd" % self.image.id
        )
        self.assertIn("<figure", md_text)
        self.assertIn("<figcaption", md_text)
        md_text = md.convert("image: [image:123 align:left size:medium]\nadasd")
        self.assertIn("Image not found", md_text)
        self.assertIn("<figcaption", md_text)

    def test_caption(self):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert(
            "[image:%s align:left]\n    this is visual" % self.image.id
        )
        self.assertIn("<figure", md_text)
        self.assertRegex(
            md_text, r'<figcaption class="caption">\s*this is visual\s*</figcaption>'
        )
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert(
            "[image:%s align:left]\n    this is visual\n    second line" % self.image.id
        )
        self.assertIn("<figure", md_text)
        self.assertRegex(
            md_text,
            r'<figcaption class="caption">\s*this is visual\s*second line\s*</figcaption>',
        )

    def check_escape(self, text_to_escape):
        md = markdown.ArticleMarkdown(article=self.root_article)
        md_text = md.convert("`%s`" % text_to_escape)
        self.assertNotIn("<figure", md_text)
        self.assertIn(text_to_escape, md_text)

    def test_escape(self):
        self.check_escape("[image:%s align:left]" % self.image.id)
        self.check_escape("image tag: [image:%s]" % self.image.id)

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)