Browse Source

Merge pull request #435 from justanr/ISS-379-customize-markdown-renderer

Implement customizable markdown renderers
Peter Justin 7 years ago
parent
commit
52ddd37a5c

+ 2 - 0
docs/hooks.rst

@@ -46,6 +46,8 @@ The hooks below are listed in the order they are called.
 .. autofunction:: flaskbb_errorhandlers
 .. autofunction:: flaskbb_load_migrations
 .. autofunction:: flaskbb_load_translations
+.. autofunction:: flaskbb_load_post_markdown_class
+.. autofunction:: flaskbb_load_nonpost_markdown_class
 .. autofunction:: flaskbb_additional_setup
 
 

+ 2 - 2
flaskbb/app.py

@@ -37,7 +37,7 @@ from flaskbb.user.models import Guest, User
 from flaskbb.utils.helpers import (app_config_from_env, crop_title,
                                    format_date, forum_is_unread,
                                    get_alembic_locations, get_flaskbb_config,
-                                   is_online, mark_online, render_markup,
+                                   is_online, mark_online,
                                    render_template, time_since, time_utcnow,
                                    topic_is_unread)
 # permission checks (here they are used for the jinja filters)
@@ -53,6 +53,7 @@ from flaskbb.utils.search import (ForumWhoosheer, PostWhoosheer,
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.translations import FlaskBBDomain
 
+from . import markup
 from .auth import views as auth_views
 from .forum import views as forum_views
 from .management import views as management_views
@@ -225,7 +226,6 @@ def configure_template_filters(app):
     """Configures the template filters."""
     filters = {}
 
-    filters['markup'] = render_markup
     filters['format_date'] = format_date
     filters['time_since'] = time_since
     filters['is_online'] = is_online

+ 42 - 16
flaskbb/utils/markup.py → flaskbb/markup.py

@@ -11,42 +11,44 @@
 import logging
 import re
 
-from flask import url_for
-
 import mistune
+from flask import url_for
+from jinja2 import Markup
+from pluggy import HookimplMarker
 from pygments import highlight
-from pygments.lexers import get_lexer_by_name
 from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_by_name
 from pygments.util import ClassNotFound
 
+impl = HookimplMarker('flaskbb')
 
 logger = logging.getLogger(__name__)
 
-
 _re_user = re.compile(r'@(\w+)', re.I)
 
 
+def userify(match):
+    value = match.group(1)
+    user = "<a href='{url}'>@{user}</a>".format(
+        url=url_for("user.profile", username=value, _external=False),
+        user=value
+    )
+    return user
+
+
 class FlaskBBRenderer(mistune.Renderer):
     """Markdown with some syntactic sugar, such as @user gettting linked
     to the user's profile.
     """
+
     def __init__(self, **kwargs):
         super(FlaskBBRenderer, self).__init__(**kwargs)
 
     def paragraph(self, text):
         """Render paragraph tags, autolinking user handles."""
 
-        def userify(match):
-            value = match.group(1)
-            user = "<a href='{url}'>@{user}</a>".format(
-                url=url_for("user.profile", username=value, _external=False),
-                user=value
-            )
-            return user
-
         text = _re_user.sub(userify, text)
-
-        return '<p>%s</p>\n' % text.strip(' ')
+        return super(FlaskBBRenderer, self).paragraph(text)
 
     def block_code(self, code, lang):
         if lang:
@@ -63,5 +65,29 @@ class FlaskBBRenderer(mistune.Renderer):
         return highlight(code, lexer, formatter)
 
 
-renderer = FlaskBBRenderer(escape=True, hard_wrap=True)
-markdown = mistune.Markdown(renderer=renderer)
+@impl
+def flaskbb_load_post_markdown_class():
+    return FlaskBBRenderer
+
+
+@impl
+def flaskbb_load_nonpost_markdown_class():
+    return FlaskBBRenderer
+
+
+@impl
+def flaskbb_jinja_directives(app):
+    render_classes = app.pluggy.hook.flaskbb_load_post_markdown_class(app=app)
+    app.jinja_env.filters['markup'] = make_renderer(render_classes)
+
+    render_classes = app.pluggy.hook.flaskbb_load_nonpost_markdown_class(
+        app=app
+    )
+    app.jinja_env.filters['nonpost_markup'] = make_renderer(render_classes)
+
+
+def make_renderer(classes):
+    RenderCls = type('FlaskBBRenderer', tuple(classes), {})
+
+    markup = mistune.Markdown(renderer=RenderCls(escape=True, hard_wrap=True))
+    return lambda text: Markup(markup.render(text))

+ 57 - 0
flaskbb/plugins/spec.py

@@ -79,6 +79,61 @@ def flaskbb_additional_setup(app, pluggy):
 
 
 @spec
+def flaskbb_load_post_markdown_class(app):
+    """
+    Hook for loading a mistune renderer child class in order to render
+    markdown on posts and user signatures. All classes returned by this hook
+    will be composed into a single class to render markdown for posts.
+
+    Since all classes will be composed together, child classes should call
+    super as appropriate and not add any new arguments to `__init__` since the
+    class will be insantiated with predetermined arguments.
+
+
+    Example::
+
+        class YellingRenderer(mistune.Renderer):
+            def paragraph(self, text):
+                return super(YellingRenderer, self).paragraph(text.upper())
+
+        @impl
+        def flaskbb_load_post_markdown_class():
+            return YellingRenderer
+
+    :param app: The application object associated with the class if needed
+    :type app: Flask
+    """
+
+
+@spec
+def flaskbb_load_nonpost_markdown_class(app):
+    """
+    Hook for loading a mistune renderer child class in order to render
+    markdown in locations other than posts, for example in category or
+    forum descriptions. All classes returned by this hook will be composed into
+    a single class to render markdown for nonpost content (e.g. forum and
+    category descriptions).
+
+    Since all classes will be composed together, child classes should call
+    super as appropriate and not add any new arguments to `__init__` since the
+    class will be insantiated with predetermined arguments.
+
+    Example::
+
+        class YellingRenderer(mistune.Renderer):
+            def paragraph(self, text):
+                return super(YellingRenderer, self).paragraph(text.upper())
+
+        @impl
+        def flaskbb_load_nonpost_markdown_class():
+            return YellingRenderer
+
+    :param app: The application object associated with the class if needed
+    :type app: Flask
+    """
+
+
+@spec
 def flaskbb_cli(cli, app):
     """Hook for registering CLI commands.
 
@@ -320,6 +375,7 @@ def flaskbb_post_reauth(user):
     :class:`PostReauthenticateHandler<flaskbb.core.auth.PostAuthenticationHandler>`
     """
 
+
 @spec
 def flaskbb_reauth_failed(user):
     """Hook called if a reauth fails.
@@ -336,6 +392,7 @@ def flaskbb_reauth_failed(user):
     :class:`ReauthenticateFailureHandler<flaskbb.core.auth.ReauthenticateFailureHandler>`
     """
 
+
 # Form hooks
 @spec
 def flaskbb_form_new_post(form):

+ 2 - 2
flaskbb/templates/forum/category_layout.html

@@ -29,7 +29,7 @@
 
                         <!-- Forum Description -->
                         <div class="forum-description">
-                            {{ forum.description|markup }}
+                            {{ forum.description|nonpost_markup }}
                         </div>
                     </div>
                 </div>
@@ -71,7 +71,7 @@
 
                         <!-- Forum Description -->
                         <div class="forum-description">
-                            {{ forum.description|markup }}
+                            {{ forum.description|nonpost_markup }}
                         </div>
 
                         <!-- Forum Moderators -->

+ 2 - 2
flaskbb/templates/forum/search_result.html

@@ -234,7 +234,7 @@
 
                             <!-- Forum Description -->
                             <div class="forum-description">
-                                {{ forum.description|markup }}
+                                {{ forum.description|nonpost_markup }}
                             </div>
                         </div>
                     </div>
@@ -274,7 +274,7 @@
 
                             <!-- Forum Description -->
                             <div class="forum-description">
-                                {{ forum.description|markup }}
+                                {{ forum.description|nonpost_markup }}
                             </div>
 
                             <!-- Forum Moderators -->

+ 2 - 2
flaskbb/templates/management/forums.html

@@ -81,7 +81,7 @@
 
                                         <!-- Forum Description -->
                                         <div class="forum-description">
-                                            {{ forum.description|markup }}
+                                            {{ forum.description|nonpost_markup }}
                                         </div>
                                     </div>
                                 </div>
@@ -127,7 +127,7 @@
 
                                         <!-- Forum Description -->
                                         <div class="forum-description">
-                                            {{ forum.description|markup }}
+                                            {{ forum.description|nonpost_markup }}
                                         </div>
 
                                         <!-- Forum Moderators -->

+ 0 - 9
flaskbb/utils/helpers.py

@@ -34,7 +34,6 @@ from flask_themes2 import get_themes_list, render_theme_template
 from flaskbb._compat import (iteritems, range_method, text_type, string_types,
                              to_bytes, to_unicode)
 from flaskbb.extensions import babel, redis_store
-from flaskbb.utils.markup import markdown
 from flaskbb.utils.settings import flaskbb_config
 from jinja2 import Markup
 from PIL import ImageFile
@@ -371,14 +370,6 @@ def crop_title(title, length=None, suffix="..."):
     return title[:length].rsplit(' ', 1)[0] + suffix
 
 
-def render_markup(text):
-    """Renders the given text as markdown
-
-    :param text: The text that should be rendered as markdown
-    """
-    return Markup(markdown.render(text))
-
-
 def is_online(user):
     """A simple check to see if the user was online within a specified
     time range

+ 12 - 13
tests/unit/utils/test_helpers.py

@@ -1,10 +1,12 @@
 # -*- coding: utf-8 -*-
 import datetime as dt
-from flaskbb.utils.helpers import slugify, forum_is_unread, topic_is_unread, \
-    crop_title, render_markup, is_online, format_date, format_quote, \
-    get_image_info, check_image, time_utcnow
-from flaskbb.utils.settings import flaskbb_config
+
 from flaskbb.forum.models import Forum
+from flaskbb.utils.helpers import (check_image, crop_title, format_date,
+                                   format_quote, forum_is_unread,
+                                   get_image_info, is_online,
+                                   slugify, time_utcnow, topic_is_unread)
+from flaskbb.utils.settings import flaskbb_config
 
 
 def test_slugify():
@@ -63,15 +65,17 @@ def test_topic_is_unread(guest, user, forum, topic, topicsread, forumsread):
     assert topic_is_unread(topic, topicsread, user, forumsread)
 
     # TopicsRead is none and the forum has never been marked as read
-    assert topic_is_unread(topic, topicsread=None, user=user,
-                           forumsread=forumsread)
+    assert topic_is_unread(
+        topic, topicsread=None, user=user, forumsread=forumsread
+    )
 
     # lets mark the forum as read
     forumsread.cleared = time_utcnow()
     forumsread.last_read = time_utcnow()
     forumsread.save()
-    assert not topic_is_unread(topic, topicsread=None, user=user,
-                               forumsread=forumsread)
+    assert not topic_is_unread(
+        topic, topicsread=None, user=user, forumsread=forumsread
+    )
 
     # disabled tracker
     flaskbb_config["TRACKER_LENGTH"] = 0
@@ -93,11 +97,6 @@ def test_crop_title(default_settings):
     assert crop_title(long_title) == "This is just a..."
 
 
-def test_render_markup(default_settings):
-    markdown = "**Bold**"
-    assert render_markup(markdown) == "<p><strong>Bold</strong></p>\n"
-
-
 def test_is_online(default_settings, user):
     assert is_online(user)
 

+ 7 - 5
tests/unit/utils/test_markup.py

@@ -1,10 +1,12 @@
-from flaskbb.utils.markup import markdown
+from flaskbb.markup import FlaskBBRenderer, make_renderer
+
+markdown = make_renderer([FlaskBBRenderer])
 
 
 def test_custom_renderer():
     # custom paragraph
     p_plain = "@sh4nks is developing flaskbb."
-    assert "/user/sh4nks" in markdown.render(p_plain)
+    assert "/user/sh4nks" in markdown(p_plain)
 
     # custom block code with pygments highlighting (jus)
     b_plain = """
@@ -18,8 +20,8 @@ print("Hello World")
 ```
 """
 
-    assert "<pre>" in markdown.render(b_plain)
-    assert "highlight" in markdown.render(b_plain_lang)
+    assert "<pre>" in markdown(b_plain)
+    assert "highlight" in markdown(b_plain_lang)
 
     # typo in language
     bad_language = """
@@ -28,6 +30,6 @@ print("Hello World")
 ```
 """
 
-    bad_language_render = markdown.render(bad_language)
+    bad_language_render = markdown(bad_language)
     assert "<pre>" in bad_language_render
     assert "highlight" not in bad_language_render