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)