Просмотр исходного кода

Switched to markdown with Emoji support. Addresses #95 and #96.

sh4nks 10 лет назад
Родитель
Сommit
d8e945e159

+ 0 - 11
flaskbb/fixtures/settings.py

@@ -17,10 +17,6 @@ def available_themes():
     return [(theme.identifier, theme.name) for theme in get_themes_list()]
 
 
-def available_markups():
-    return [('bbcode', 'BBCode'), ('markdown', 'Markdown')]
-
-
 def available_avatar_types():
     return [("image/png", "PNG"), ("image/jpeg", "JPG"), ("image/gif", "GIF")]
 
@@ -96,13 +92,6 @@ fixture = (
                 'name':         "Tracker length",
                 'description':  "The days for how long the forum should deal with unread topics. 0 to disable it."
             }),
-            ('markup_type', {
-                'value':        "bbcode",
-                'value_type':   "select",
-                'extra':        {'choices': available_markups},
-                'name':         "Post markup",
-                'description':  "Select post markup type."
-            }),
             ('avatar_height', {
                 'value':        150,
                 'value_type':   "integer",

+ 59 - 59
flaskbb/static/css/code.css

@@ -1,59 +1,59 @@
-.code { background: #f8f8f8; font-size:14px;}
-.code .c { color: #008800; font-style: italic } /* Comment */
-.code .err { border: 1px solid #FF0000 } /* Error */
-.code .k { color: #AA22FF; font-weight: bold } /* Keyword */
-.code .o { color: #666666 } /* Operator */
-.code .cm { color: #008800; font-style: italic } /* Comment.Multiline */
-.code .cp { color: #008800 } /* Comment.Preproc */
-.code .c1 { color: #008800; font-style: italic } /* Comment.Single */
-.code .cs { color: #008800; font-weight: bold } /* Comment.Special */
-.code .gd { color: #A00000 } /* Generic.Deleted */
-.code .ge { font-style: italic } /* Generic.Emph */
-.code .gr { color: #FF0000 } /* Generic.Error */
-.code .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.code .gi { color: #00A000 } /* Generic.Inserted */
-.code .go { color: #808080 } /* Generic.Output */
-.code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
-.code .gs { font-weight: bold } /* Generic.Strong */
-.code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.code .gt { color: #0040D0 } /* Generic.Traceback */
-.code .kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */
-.code .kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */
-.code .kp { color: #AA22FF } /* Keyword.Pseudo */
-.code .kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */
-.code .kt { color: #AA22FF; font-weight: bold } /* Keyword.Type */
-.code .m { color: #666666 } /* Literal.Number */
-.code .s { color: #BB4444 } /* Literal.String */
-.code .na { color: #BB4444 } /* Name.Attribute */
-.code .nb { color: #AA22FF } /* Name.Builtin */
-.code .nc { color: #0000FF } /* Name.Class */
-.code .no { color: #880000 } /* Name.Constant */
-.code .nd { color: #AA22FF } /* Name.Decorator */
-.code .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
-.code .nf { color: #00A000 } /* Name.Function */
-.code .nl { color: #A0A000 } /* Name.Label */
-.code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
-.code .nt { color: #008000; font-weight: bold } /* Name.Tag */
-.code .nv { color: #B8860B } /* Name.Variable */
-.code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
-.code .mf { color: #666666 } /* Literal.Number.Float */
-.code .mh { color: #666666 } /* Literal.Number.Hex */
-.code .mi { color: #666666 } /* Literal.Number.Integer */
-.code .mo { color: #666666 } /* Literal.Number.Oct */
-.code .sb { color: #BB4444 } /* Literal.String.Backtick */
-.code .sc { color: #BB4444 } /* Literal.String.Char */
-.code .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */
-.code .s2 { color: #BB4444 } /* Literal.String.Double */
-.code .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
-.code .sh { color: #BB4444 } /* Literal.String.Heredoc */
-.code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
-.code .sx { color: #008000 } /* Literal.String.Other */
-.code .sr { color: #BB6688 } /* Literal.String.Regex */
-.code .s1 { color: #BB4444 } /* Literal.String.Single */
-.code .ss { color: #B8860B } /* Literal.String.Symbol */
-.code .bp { color: #AA22FF } /* Name.Builtin.Pseudo */
-.code .vc { color: #B8860B } /* Name.Variable.Class */
-.code .vg { color: #B8860B } /* Name.Variable.Global */
-.code .vi { color: #B8860B } /* Name.Variable.Instance */
-.code .il { color: #666666 } /* Literal.Number.Integer.Long */
+.highlight { background: #f8f8f8; font-size:14px;}
+.highlight .c { color: #008800; font-style: italic } /* Comment */
+.highlight .err { border: 1px solid #FF0000 } /* Error */
+.highlight .k { color: #AA22FF; font-weight: bold } /* Keyword */
+.highlight .o { color: #666666 } /* Operator */
+.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #008800 } /* Comment.Preproc */
+.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #008800; font-weight: bold } /* Comment.Special */
+.highlight .gd { color: #A00000 } /* Generic.Deleted */
+.highlight .ge { font-style: italic } /* Generic.Emph */
+.highlight .gr { color: #FF0000 } /* Generic.Error */
+.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
+.highlight .gi { color: #00A000 } /* Generic.Inserted */
+.highlight .go { color: #808080 } /* Generic.Output */
+.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
+.highlight .gs { font-weight: bold } /* Generic.Strong */
+.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
+.highlight .gt { color: #0040D0 } /* Generic.Traceback */
+.highlight .kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */
+.highlight .kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */
+.highlight .kp { color: #AA22FF } /* Keyword.Pseudo */
+.highlight .kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */
+.highlight .kt { color: #AA22FF; font-weight: bold } /* Keyword.Type */
+.highlight .m { color: #666666 } /* Literal.Number */
+.highlight .s { color: #BB4444 } /* Literal.String */
+.highlight .na { color: #BB4444 } /* Name.Attribute */
+.highlight .nb { color: #AA22FF } /* Name.Builtin */
+.highlight .nc { color: #0000FF } /* Name.Class */
+.highlight .no { color: #880000 } /* Name.Constant */
+.highlight .nd { color: #AA22FF } /* Name.Decorator */
+.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
+.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
+.highlight .nf { color: #00A000 } /* Name.Function */
+.highlight .nl { color: #A0A000 } /* Name.Label */
+.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
+.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
+.highlight .nv { color: #B8860B } /* Name.Variable */
+.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
+.highlight .mf { color: #666666 } /* Literal.Number.Float */
+.highlight .mh { color: #666666 } /* Literal.Number.Hex */
+.highlight .mi { color: #666666 } /* Literal.Number.Integer */
+.highlight .mo { color: #666666 } /* Literal.Number.Oct */
+.highlight .sb { color: #BB4444 } /* Literal.String.Backtick */
+.highlight .sc { color: #BB4444 } /* Literal.String.Char */
+.highlight .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */
+.highlight .s2 { color: #BB4444 } /* Literal.String.Double */
+.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
+.highlight .sh { color: #BB4444 } /* Literal.String.Heredoc */
+.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
+.highlight .sx { color: #008000 } /* Literal.String.Other */
+.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
+.highlight .s1 { color: #BB4444 } /* Literal.String.Single */
+.highlight .ss { color: #B8860B } /* Literal.String.Symbol */
+.highlight .bp { color: #AA22FF } /* Name.Builtin.Pseudo */
+.highlight .vc { color: #B8860B } /* Name.Variable.Class */
+.highlight .vg { color: #B8860B } /* Name.Variable.Global */
+.highlight .vi { color: #B8860B } /* Name.Variable.Instance */
+.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */

+ 20 - 20
flaskbb/static/css/flaskbb.css

@@ -1,30 +1,24 @@
-html,
-body {
-  height: 100%; /* The html and body elements cannot have any padding or margin. */
-}
-
-/* Wrapper for page content to push down footer */
-#wrap {
+/* Sticky footer styles
+-------------------------------------------------- */
+html {
+  position: relative;
   min-height: 100%;
-  height: auto !important;
-  height: 100%; /* Negative indent footer by its height */
-  margin: 0 auto -60px;
-  padding: 0 0 60px; /* Pad bottom by footer height */
 }
-
-/* Set the fixed height of the footer here */
-#footer {
+body {
+  /* Margin bottom by footer height */
+  margin-bottom: 60px;
+}
+.footer {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  /* Set the fixed height of the footer here */
   height: 60px;
   background-color: #f5f5f5;
 }
-.container .credit {
+.container .text-muted {
   margin: 20px 0;
 }
-#footer > .container {
-  margin: 0 auto 15px;
-  padding-left: 15px;
-  padding-right: 15px;
-}
 
 .pagination {
   margin: 0;
@@ -201,3 +195,9 @@ margin-bottom: 0px;
 .inline-form .btn-link {
   padding: 0px;
 }
+
+.emoji {
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}

+ 0 - 2
flaskbb/templates/layout.html

@@ -26,7 +26,6 @@
     </head>
 
     <body>
-        <div id="wrap">
         {% block navigation %}
         {%- from theme("macros.html") import topnav with context -%}
         <!-- Navigation -->
@@ -112,7 +111,6 @@
                 {% block content %}
                 {% endblock %}
             </div> <!-- /container -->
-        </div> <!-- End wrap -->
 
             {% block footer %}
             <div id="footer">

+ 20 - 21
flaskbb/themes/bootstrap2/static/css/flaskbb.css

@@ -1,30 +1,24 @@
-html,
-body {
-  height: 100%; /* The html and body elements cannot have any padding or margin. */
-}
-
-/* Wrapper for page content to push down footer */
-#wrap {
+/* Sticky footer styles
+-------------------------------------------------- */
+html {
+  position: relative;
   min-height: 100%;
-  height: auto !important;
-  height: 100%; /* Negative indent footer by its height */
-  margin: 0 auto -60px;
-  padding: 0 0 60px; /* Pad bottom by footer height */
 }
-
-/* Set the fixed height of the footer here */
-#footer {
+body {
+  /* Margin bottom by footer height */
+  margin-bottom: 60px;
+}
+.footer {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  /* Set the fixed height of the footer here */
   height: 60px;
   background-color: #f5f5f5;
 }
-.container .credit {
+.container .text-muted {
   margin: 20px 0;
 }
-#footer > .container {
-  margin: 0 auto 15px;
-  padding-left: 15px;
-  padding-right: 15px;
-}
 
 .pagination {
   margin: 0;
@@ -227,7 +221,6 @@ margin-bottom: 0px;
     height: auto;
 }
 
-
 .inline-form {
   display: inline;
   padding: 0px;
@@ -236,3 +229,9 @@ margin-bottom: 0px;
 .inline-form .btn-link {
   padding: 0px;
 }
+
+.emoji {
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}

+ 3 - 5
flaskbb/themes/bootstrap2/templates/layout.html

@@ -19,19 +19,18 @@
 
         {% block css %}
         <link rel="stylesheet" href="{{ url_for('static', filename='css/code.css') }}">
-        <link rel="stylesheet" href="{{ url_for('css/bootstrap.min.css') }}">
+        <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
         <link rel="stylesheet" href="{{ theme_static('css/bootstrap-theme.min.css') }}">
-        <link rel="stylesheet" href="{{ url_for('css/font-awesome.min.css') }}">
+        <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}">
         <link rel="stylesheet" href="{{ theme_static('css/flaskbb.css') }}">
         {% endblock %}
     </head>
 
     <body>
-        <div id="wrap">
         {% block navigation %}
         {%- from theme("macros.html") import topnav with context -%}
         <!-- Navigation -->
-            <nav class="navbar navbar-default navbar-static-top">
+            <nav class="navbar navbar-inverse navbar-static-top">
                 <div class="container">
                     <div class="navbar-header">
                         <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
@@ -113,7 +112,6 @@
                 {% block content %}
                 {% endblock %}
             </div> <!-- /container -->
-        </div> <!-- End wrap -->
 
             {% block footer %}
             <div id="footer">

+ 21 - 20
flaskbb/themes/bootstrap3/static/css/flaskbb.css

@@ -1,30 +1,25 @@
-html,
-body {
-  height: 100%; /* The html and body elements cannot have any padding or margin. */
-}
-
-/* Wrapper for page content to push down footer */
-#wrap {
+/* Sticky footer styles
+-------------------------------------------------- */
+html {
+  position: relative;
   min-height: 100%;
-  height: auto !important;
-  height: 100%; /* Negative indent footer by its height */
-  margin: 0 auto -60px;
-  padding: 0 0 60px; /* Pad bottom by footer height */
 }
-
-/* Set the fixed height of the footer here */
-#footer {
+body {
+  /* Margin bottom by footer height */
+  margin-bottom: 60px;
+}
+.footer {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  /* Set the fixed height of the footer here */
   height: 60px;
   background-color: #f5f5f5;
 }
-.container .credit {
+.container .text-muted {
   margin: 20px 0;
 }
-#footer > .container {
-  margin: 0 auto 15px;
-  padding-left: 15px;
-  padding-right: 15px;
-}
+
 
 .pagination {
   margin: 0;
@@ -239,3 +234,9 @@ margin-bottom: 0px;
 .inline-form .btn-link {
   padding: 0px;
 }
+
+.emoji {
+  vertical-align: middle;
+  width: 20px;
+  height: 20px;
+}

+ 6 - 8
flaskbb/themes/bootstrap3/templates/layout.html

@@ -19,14 +19,13 @@
 
         {% block css %}
         <link rel="stylesheet" href="{{ url_for('static', filename='css/code.css') }}">
-        <link rel="stylesheet" href="{{ url_for('css/bootstrap.min.css') }}">
-        <link rel="stylesheet" href="{{ url_for('css/font-awesome.min.css') }}">
-        <link rel="stylesheet" href="{{ url_for('css/flaskbb.css') }}">
+        <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
+        <link rel="stylesheet" href="{{ url_for('static', filename='css/font-awesome.min.css') }}">
+        <link rel="stylesheet" href="{{ theme_static('css/flaskbb.css') }}">
         {% endblock %}
     </head>
 
     <body>
-        <div id="wrap">
         {% block navigation %}
         {%- from theme("macros.html") import topnav with context -%}
         <!-- Navigation -->
@@ -112,13 +111,12 @@
                 {% block content %}
                 {% endblock %}
             </div> <!-- /container -->
-        </div> <!-- End wrap -->
 
             {% block footer %}
-            <div id="footer">
+            <div class="footer">
                 <div class="container">
-                    <p class="text-muted credit pull-left">powered by <a href="http://flask.pocoo.org">Flask</a></p>
-                    <p class="text-muted credit pull-right">&copy; 2013 - <a href="http://flaskbb.org">FlaskBB.org</a></p>
+                    <p class="text-muted pull-left">powered by <a href="http://flask.pocoo.org">Flask</a></p>
+                    <p class="text-muted pull-right">&copy; 2013 - <a href="http://flaskbb.org">FlaskBB.org</a></p>
                 </div>
             </div>
             {% endblock %}

+ 9 - 23
flaskbb/utils/helpers.py

@@ -21,13 +21,12 @@ from flask import session, url_for
 from babel.dates import format_timedelta
 from flask_themes2 import render_theme_template
 from flask_login import current_user
-from postmarkup import render_bbcode
-from markdown2 import markdown as render_markdown
 import unidecode
 
 from flaskbb._compat import range_method, text_type
 from flaskbb.extensions import redis_store
 from flaskbb.utils.settings import flaskbb_config
+from flaskbb.utils.markup import markdown
 
 _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
 
@@ -273,15 +272,11 @@ def crop_title(title, suffix="..."):
 
 
 def render_markup(text):
-    """Renders the given text as bbcode
+    """Renders the given text as markdown
 
-    :param text: The text that should be rendered as bbcode
+    :param text: The text that should be rendered as markdown
     """
-    if flaskbb_config['MARKUP_TYPE'] == 'bbcode':
-        return render_bbcode(text)
-    elif flaskbb_config['MARKUP_TYPE'] == 'markdown':
-        return render_markdown(text, extras=['tables'])
-    return text
+    return markdown.render(text)
 
 
 def is_online(user):
@@ -333,21 +328,12 @@ def format_quote(post):
 
     :param post: The quoted post.
     """
-    if flaskbb_config['MARKUP_TYPE'] == 'markdown':
-        profile_url = url_for('user.profile', username=post.username)
-        content = "\n> ".join(post.content.strip().split('\n'))
-        quote = "**[{post.username}]({profile_url}) wrote:**\n> {content}\n".\
-                format(post=post, profile_url=profile_url, content=content)
+    profile_url = url_for('user.profile', username=post.username)
+    content = "\n> ".join(post.content.strip().split('\n'))
+    quote = "**[{post.username}]({profile_url}) wrote:**\n> {content}\n".\
+            format(post=post, profile_url=profile_url, content=content)
 
-        return quote
-    else:
-        profile_url = url_for('user.profile', username=post.username,
-                              _external=True)
-        # just ignore this long line :P
-        quote = '[b][url={profile_url}]{post.username}[/url] wrote:[/b][quote]{post.content}[/quote]\n'.\
-                format(post=post, profile_url=profile_url)
-
-        return quote
+    return quote
 
 
 def get_image_info(url):

+ 84 - 0
flaskbb/utils/markup.py

@@ -0,0 +1,84 @@
+import os
+import re
+
+from flask import url_for
+
+import mistune
+from pygments import highlight
+from pygments.lexers import get_lexer_by_name
+from pygments.formatters import HtmlFormatter
+
+
+_re_emoji = re.compile(r':([a-z0-9\+\-_]+):', re.I)
+_re_user = re.compile(r'@(\w+)', re.I)
+
+
+def collect_emojis():
+    """Returns a dictionary containing all emojis with their
+    name and filename. If the folder doesn't exist it returns a empty
+    dictionary.
+    """
+    emojis = dict()
+    full_path = os.path.join(os.path.abspath("flaskbb"), "static", "emoji")
+    # return an empty dictionary if the path doesn't exist
+    if not os.path.exists(full_path):
+        return emojis
+
+    for emoji in os.listdir(full_path):
+        emojis[emoji.split(".")[0]] = emoji
+
+    return emojis
+
+EMOJIS = collect_emojis()
+
+
+class FlaskBBRenderer(mistune.Renderer):
+    """Markdown with some syntetic sugar such as @user gets linked to the
+    user's profile and emoji support.
+    """
+    def __init__(self, **kwargs):
+        super(FlaskBBRenderer, self).__init__(**kwargs)
+
+    def paragraph(self, text):
+        """Rendering paragraph tags. Like ``<p>`` with emoji support."""
+
+        def emojify(match):
+            value = match.group(1)
+
+            if value in EMOJIS:
+                filename = url_for(
+                    "static",
+                    filename="emoji/{}".format(EMOJIS[value])
+                )
+
+                emoji = "<img class='{css}' alt='{alt}' src='{src}' />".format(
+                    css="emoji", alt=value,
+                    src=filename
+                )
+                return emoji
+            return match.group(0)
+
+        def userify(match):
+            value = match.group(1)
+            user = "<a href='{url}'>@{user}</a>".format(
+                url=url_for("user.profile", username=value),
+                user=value
+            )
+            return user
+
+        text = _re_emoji.sub(emojify, text)
+        text = _re_user.sub(userify, text)
+
+        return '<p>%s</p>\n' % text.strip(' ')
+
+    def block_code(self, code, lang):
+        if not lang:
+            return '\n<pre><code>%s</code></pre>\n' % \
+                mistune.escape(code)
+        lexer = get_lexer_by_name(lang, stripall=True)
+        formatter = HtmlFormatter()
+        return highlight(code, lexer, formatter)
+
+
+renderer = FlaskBBRenderer()
+markdown = mistune.Markdown(renderer=renderer)

+ 21 - 0
manage.py

@@ -20,6 +20,7 @@
 import sys
 import os
 import subprocess
+import requests
 
 from flask import current_app
 from werkzeug.utils import import_string
@@ -279,5 +280,25 @@ def compile_plugin_translations(plugin):
     subprocess.call(["pybabel", "compile", "-d", translations_folder])
 
 
+@manager.command
+def download_emoji():
+    """Downloads emojis from emoji-cheat-sheet.com."""
+    HOSTNAME = "https://api.github.com"
+    REPO = "/repos/arvida/emoji-cheat-sheet.com/contents/public/graphics/emojis"
+    FULL_URL = "{}{}".format(HOSTNAME, REPO)
+    DOWNLOAD_PATH = os.path.join(app.static_folder, "emoji")
+
+    response = requests.get(FULL_URL)
+
+    for image in response.json():
+        if not os.path.exists(os.path.abspath(DOWNLOAD_PATH)):
+            print "%s does not exist." % os.path.abspath(DOWNLOAD_PATH)
+            sys.exit(1)
+
+        full_path = os.path.join(DOWNLOAD_PATH, image["name"])
+        f = open(full_path, 'wb')
+        f.write(requests.get(image["download_url"]).content)
+        f.close()
+
 if __name__ == "__main__":
     manager.run()