diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py
index a546c2374085f0e327e885cbccb0a9cfa2725b61..385cb47f0aac8d7c8779a8fb4f38a1977ce56216 100644
--- a/SciPost_v1/settings/base.py
+++ b/SciPost_v1/settings/base.py
@@ -100,6 +100,7 @@ INSTALLED_APPS = (
     'journals',
     'mailing_lists',
     'mails',
+    'markup',
     'news',
     'notifications',
     'partners',
diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py
index 5aec46b25cba7cda945c33cfda38c2e45f6a8633..384dfef2b88c526ac39cea682c71055205af80f3 100644
--- a/SciPost_v1/urls.py
+++ b/SciPost_v1/urls.py
@@ -54,6 +54,7 @@ urlpatterns = [
     url(r'^invitations/', include('invitations.urls', namespace="invitations")),
     url(r'^journals/', include('journals.urls.general', namespace="journals")),
     url(r'^mailing_list/', include('mailing_lists.urls', namespace="mailing_lists")),
+    url(r'^markup/', include('markup.urls', namespace='markup')),
     url(r'^submissions/', include('submissions.urls', namespace="submissions")),
     url(r'^submission/', include('submissions.urls', namespace="_submissions")),
     url(r'^theses/', include('theses.urls', namespace="theses")),
diff --git a/common/forms.py b/common/forms.py
index 9a532ad6c551c00b4fd665e08f1a5abeceafb282..f3d3090c0f03838eb9217103ab618ee777a45db2 100644
--- a/common/forms.py
+++ b/common/forms.py
@@ -4,16 +4,13 @@ __license__ = "AGPL v3"
 
 import calendar
 import datetime
-from docutils.core import publish_parts
 import re
 
 from django import forms
 from django.forms.widgets import Widget, Select
 from django.utils.dates import MONTHS
-from django.utils.encoding import force_text
 from django.utils.safestring import mark_safe
 
-from .utils import detect_markup_language
 
 
 __all__ = ('MonthYearWidget',)
@@ -124,51 +121,3 @@ class MonthYearWidget(Widget):
 class ModelChoiceFieldwithid(forms.ModelChoiceField):
     def label_from_instance(self, obj):
         return '%s (id = %i)' % (super().label_from_instance(obj), obj.id)
-
-
-
-class MarkupTextForm(forms.Form):
-    markup_text = forms.CharField()
-
-    def get_processed_markup(self):
-        text = self.cleaned_data['markup_text']
-
-        # Detect text format
-        markup_detector = detect_markup_language(text)
-        language = markup_detector['language']
-        print('language: %s' % language)
-
-        if markup_detector['errors']:
-            return markup_detector
-
-        if language == 'reStructuredText':
-            # This performs the same actions as the restructuredtext filter of app scipost
-            from io import StringIO
-            warnStream = StringIO()
-            try:
-                parts = publish_parts(
-                    source=text,
-                    writer_name='html5_polyglot',
-                    settings_overrides={
-                        'math_output': 'MathJax https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML,Safe',
-                        'initial_header_level': 1,
-                        'doctitle_xform': False,
-                        'raw_enabled': False,
-                        'file_insertion_enabled': False,
-                        'warning_stream': warnStream})
-                return {
-                    'language': language,
-                    'processed_markup': mark_safe(force_text(parts['html_body'])),
-                }
-            except:
-                pass
-            return {
-                'language': language,
-                'errors': warnStream.getvalue()
-            }
-        # at this point, language is assumed to be plain text
-        from django.template.defaultfilters import linebreaksbr
-        return {
-            'language': language,
-            'processed_markup': linebreaksbr(text)
-            }
diff --git a/common/templatetags/process_markup.py b/common/templatetags/process_markup.py
deleted file mode 100644
index d004300032635211563e91308447efc6a98f69be..0000000000000000000000000000000000000000
--- a/common/templatetags/process_markup.py
+++ /dev/null
@@ -1,46 +0,0 @@
-__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
-__license__ = "AGPL v3"
-
-
-from docutils.core import publish_parts
-from io import StringIO
-
-from django import template
-from django.template.defaultfilters import linebreaksbr
-from django.utils.encoding import force_text
-from django.utils.safestring import mark_safe
-
-from ..utils import detect_markup_language
-
-register = template.Library()
-
-
-@register.filter(name='process_markup')
-def process_markup(text):
-    if not text:
-        return ''
-
-    markup_detector = detect_markup_language(text)
-
-    if markup_detector['errors']:
-        return markup_detector['errors']
-
-    if markup_detector['language'] == 'reStructuredText':
-        warnStream = StringIO()
-        try:
-            parts = publish_parts(
-                source=text,
-                writer_name='html5_polyglot',
-                settings_overrides={
-                    'math_output': 'MathJax  https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML,Safe',
-                    'initial_header_level': 1,
-                    'doctitle_xform': False,
-                    'raw_enabled': False,
-                    'file_insertion_enabled': False,
-                    'warning_stream': warnStream
-                })
-            return mark_safe(force_text(parts['html_body']))
-        except:
-            return warnStream.getvalue()
-    else:
-        return linebreaksbr(text)
diff --git a/common/utils.py b/common/utils.py
index 2daae504346a926cf255beaafb5e45afd6584037..cf35641406916f71827d50b0789525ca6c9ce2a6 100644
--- a/common/utils.py
+++ b/common/utils.py
@@ -3,7 +3,6 @@ __license__ = "AGPL v3"
 
 
 from datetime import timedelta
-import re
 
 from django.core.mail import EmailMultiAlternatives
 from django.db.models import Q
@@ -135,125 +134,3 @@ class BaseMailUtil(object):
             reply_to=[cls.mail_sender])
         email.attach_alternative(html_message, 'text/html')
         email.send(fail_silently=False)
-
-
-def detect_markup_language(text):
-    """
-    Detect which markup language is being used.
-
-    This method returns a dictionary containing:
-
-    * language
-    * errors
-
-    where ``language`` can be one of: plain, reStructuredText
-
-    The criteria used are:
-
-    * if the ``math`` role or directive is found together with $...$, return error
-    * if the ``math`` role or directive is found, return ReST
-
-    Assumptions:
-
-    * MathJax is set up with $...$ for inline, \[...\] for online equations.
-    """
-
-    # Inline maths
-    inline_math = re.search("\$[^$]+\$", text)
-    # if inline_math:
-    #     print('inline math: %s' % inline_math.group(0))
-
-    # Online maths is of the form \[ ... \]
-    # The re.DOTALL is to also capture newline chars with the . (any single character)
-    online_math = re.search(r'[\\][[].+[\\][\]]', text, re.DOTALL)
-    # if online_math:
-    #     print('online math: %s' % online_math.group(0))
-
-    rst_math = '.. math::' in text or ':math:`' in text
-
-    # Normal inline/online maths cannot be used simultaneously with ReST math.
-    # If this is detected, language is set to plain, and errors are reported.
-    # Otherwise if math present in ReST but not in/online math, assume ReST.
-    if rst_math:
-        if inline_math:
-            return {
-                'language': 'plain',
-                'errors': ('Cannot determine whether this is plain text or reStructuredText.\n\n'
-                           'You have mixed inline maths ($...$) with reStructuredText markup.'
-                           '\n\nPlease use one or the other, but not both!')
-            }
-        elif online_math:
-            return {
-                'language': 'plain',
-                'errors': ('Cannot determine whether this is plain text or reStructuredText.\n\n'
-                           'You have mixed online maths (\[...\]) with reStructuredText markup.'
-                           '\n\nPlease use one or the other, but not both!')
-            }
-        else: # assume ReST
-            return {
-                'language': 'reStructuredText',
-                'errors': None
-            }
-
-    # reStructuredText header patterns
-    rst_header_patterns = [
-        "^#{2,}$", "^\*{2,}$", "^={2,}$", "^-{2,}$", "^\^{2,}$", "^\"{2,}$",]
-    # See list of reStructuredText directives at
-    # http://docutils.sourceforge.net/0.4/docs/ref/rst/directives.html
-    # We don't include the math one here since we covered it above.
-    rst_directives = [
-        "attention", "caution", "danger", "error", "hint", "important", "note", "tip",
-        "warning", "admonition",
-        "topic", "sidebar", "parsed-literal", "rubric", "epigraph", "highlights",
-        "pull-quote", "compound", "container",
-        "table", "csv-table", "list-table",
-        "contents", "sectnum", "section-autonumbering", "header", "footer",
-        "target-notes",
-        "replace", "unicode", "date", "class", "role", "default-role",
-    ]
-    # See list at http://docutils.sourceforge.net/0.4/docs/ref/rst/roles.html
-    rst_roles = [
-        "emphasis", "literal", "pep-reference", "rfc-reference",
-        "strong", "subscript", "superscript", "title-reference",
-    ]
-
-    nr_rst_headers = 0
-    for header_pattern in rst_header_patterns:
-        matches = re.findall(header_pattern, text, re.MULTILINE)
-        print ('%s matched %d times' % (header_pattern, len(matches)))
-        nr_rst_headers += len(matches)
-
-    nr_rst_directives = 0
-    for directive in rst_directives:
-        if ('.. %s::' % directive) in text:
-            nr_rst_directives += 1
-
-    nr_rst_roles = 0
-    for role in rst_roles:
-        if (':%s:`' % role) in text:
-            nr_rst_roles += 1
-
-    if (nr_rst_headers > 0 or nr_rst_directives > 0 or nr_rst_roles > 0):
-        if inline_math:
-            return {
-                'language': 'plain',
-                'errors': ('Cannot determine whether this is plain text or reStructuredText.\n\n'
-                           'You have mixed inline maths ($...$) with reStructuredText markup.'
-                           '\n\nPlease use one or the other, but not both!')
-            }
-        elif online_math:
-            return {
-                'language': 'plain',
-                'errors': ('Cannot determine whether this is plain text or reStructuredText.\n\n'
-                           'You have mixed online maths (\[...\]) with reStructuredText markup.'
-                           '\n\nPlease use one or the other, but not both!')
-            }
-        else:
-            return {
-                'language': 'reStructuredText',
-                'errors': None
-            }
-    return {
-        'language': 'plain',
-        'errors': None
-    }
diff --git a/helpdesk/migrations/0010_auto_20190620_0817.py b/helpdesk/migrations/0010_auto_20190620_0817.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3b4ec7a3bf9d569f003e5e6b7708d11a013d019
--- /dev/null
+++ b/helpdesk/migrations/0010_auto_20190620_0817.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.1.8 on 2019-06-20 06:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('helpdesk', '0009_auto_20190317_0917'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='queue',
+            name='description',
+            field=models.TextField(help_text='You can use plain text, Markdown or reStructuredText; see our <a href="/markup/help/" target="_blank">markup help</a> pages.'),
+        ),
+        migrations.AlterField(
+            model_name='ticket',
+            name='description',
+            field=models.TextField(help_text='You can use plain text, Markdown or reStructuredText; see our <a href="/markup/help/" target="_blank">markup help</a> pages.'),
+        ),
+    ]
diff --git a/helpdesk/models.py b/helpdesk/models.py
index f220a85b2ce5e84a457fb4b0b22d3d855f87a8f9..31a04016138c56939d07faa422e9874c6e9c48a7 100644
--- a/helpdesk/models.py
+++ b/helpdesk/models.py
@@ -34,7 +34,9 @@ class Queue(models.Model):
     """
     name = models.CharField(max_length=64)
     slug = models.SlugField(allow_unicode=True, unique=True)
-    description = models.TextField()
+    description = models.TextField(
+        help_text=('You can use plain text, Markdown or reStructuredText; see our '
+                   '<a href="/markup/help/" target="_blank">markup help</a> pages.'))
     managing_group = models.ForeignKey('auth.Group', on_delete=models.CASCADE,
                                        related_name='managed_queues')
     response_groups = models.ManyToManyField('auth.Group')
@@ -99,11 +101,8 @@ class Ticket(models.Model):
                                          'seems most appropriate for your issue'))
     title = models.CharField(max_length=64, default='')
     description = models.TextField(
-        help_text=(
-            'You can use ReStructuredText, see a '
-            '<a href="https://devguide.python.org/documenting/#restructuredtext-primer" '
-            'target="_blank">primer on python.org</a>')
-        )
+        help_text=('You can use plain text, Markdown or reStructuredText; see our '
+                   '<a href="/markup/help/" target="_blank">markup help</a> pages.'))
     publicly_visible = models.BooleanField(
         default=False,
         help_text=('Do you agree with this Ticket being made publicly visible '
diff --git a/helpdesk/templates/helpdesk/followup_form.html b/helpdesk/templates/helpdesk/followup_form.html
index 0f31707e50ca2e0e370530ad49b2c2a54d57d2fa..68aa84c61aeee3f305c4da8d88f795217e8bdbdd 100644
--- a/helpdesk/templates/helpdesk/followup_form.html
+++ b/helpdesk/templates/helpdesk/followup_form.html
@@ -1,7 +1,6 @@
 {% extends 'helpdesk/base.html' %}
 
 {% load bootstrap %}
-{% load restructuredtext %}
 
 
 {% block breadcrumb_items %}
diff --git a/helpdesk/templates/helpdesk/helpdesk.html b/helpdesk/templates/helpdesk/helpdesk.html
index 44f5821abc4e28d4c42350da0cf51fe8959ed2da..6b9e6ef62671a0a59d58d89a1c00cd328c469ccd 100644
--- a/helpdesk/templates/helpdesk/helpdesk.html
+++ b/helpdesk/templates/helpdesk/helpdesk.html
@@ -1,8 +1,6 @@
 {% extends 'helpdesk/base.html' %}
 
 {% load bootstrap %}
-{% load restructuredtext %}
-
 
 {% block breadcrumb_items %}
   {{ block.super }}
diff --git a/helpdesk/templates/helpdesk/queue_card.html b/helpdesk/templates/helpdesk/queue_card.html
index 3ae3cce7e2b18378e05bba088101b5f9da537e1a..f9ecfc1748bb24cecd9f778290ea76d40bdcbe26 100644
--- a/helpdesk/templates/helpdesk/queue_card.html
+++ b/helpdesk/templates/helpdesk/queue_card.html
@@ -1,5 +1,5 @@
 {% load bootstrap %}
-{% load restructuredtext %}
+{% load automarkup %}
 
 <div class="card">
   <div class="card-header">
@@ -9,7 +9,7 @@
     </div>
   </div>
   <div class="card-body">
-    {{ queue.description|restructuredtext }}
+    {{ queue.description|automarkup }}
     {% if queue.sub_queues.all|length > 0 %}
       <hr/>
       <p>Sub-Queues:</p>
diff --git a/helpdesk/templates/helpdesk/queue_confirm_delete.html b/helpdesk/templates/helpdesk/queue_confirm_delete.html
index b0b3a12b0fbd28858eb5933bee4ba234811c2091..8084179097718b4b5515ee96d7b5ec9aa1c35605 100644
--- a/helpdesk/templates/helpdesk/queue_confirm_delete.html
+++ b/helpdesk/templates/helpdesk/queue_confirm_delete.html
@@ -1,7 +1,7 @@
 {% extends 'helpdesk/base.html' %}
 
 {% load bootstrap %}
-{% load restructuredtext %}
+{% load automarkup %}
 
 {% block pagetitle %}: Delete Queue{% endblock pagetitle %}
 
@@ -11,7 +11,7 @@
       <h1 class="highlight">Delete Queue {{ object.name }}</h1>
 
       <h3 class="highlight">Description</h3>
-      {{ object.description|restructuredtext }}
+      {{ object.description|automarkup }}
 
       <h3 class="highlight">Tickets</h3>
       {% include 'helpdesk/tickets_table.html' with queue=object %}
diff --git a/helpdesk/templates/helpdesk/queue_detail.html b/helpdesk/templates/helpdesk/queue_detail.html
index 53206de02e8654e4ccd55eac5e5c41efe40bb37e..0e9e6ed75a374fa0f998ed67e21bd8392d5d4892 100644
--- a/helpdesk/templates/helpdesk/queue_detail.html
+++ b/helpdesk/templates/helpdesk/queue_detail.html
@@ -2,7 +2,7 @@
 
 {% load bootstrap %}
 {% load guardian_tags %}
-{% load restructuredtext %}
+{% load automarkup %}
 
 {% block breadcrumb_items %}
   {{ block.super }}
@@ -85,7 +85,7 @@
       {% endif %}
 
       <h3 class="highlight">Description</h3>
-      {{ queue.description|restructuredtext }}
+      {{ queue.description|automarkup }}
 
       <h3 class="highlight">Tickets in this Queue</h3>
       {% include 'helpdesk/tickets_table.html' with tickets=queue.tickets.all %}
diff --git a/helpdesk/templates/helpdesk/ticket_card.html b/helpdesk/templates/helpdesk/ticket_card.html
index 9837c0b6cb77e3ddbe56065f96feab27b7d3fa42..d52043986fe734ec9fec897e82bd7dd1e2d2f588 100644
--- a/helpdesk/templates/helpdesk/ticket_card.html
+++ b/helpdesk/templates/helpdesk/ticket_card.html
@@ -1,5 +1,5 @@
 {% load bootstrap %}
-{% load process_markup %}
+{% load automarkup %}
 {% load scipost_extras %}
 
 <div class="card">
@@ -23,7 +23,7 @@
       <tbody>
 	<tr>
 	  <th>Description</th>
-	  <td>{{ ticket.description|process_markup }}</td>
+	  <td>{{ ticket.description|automarkup }}</td>
 	</tr>
 	<tr>
 	  <th>Defined on</th>
@@ -63,7 +63,7 @@
 		      {{ followup.by.get_full_name }} on {{ followup.timestamp }}
 		    </div>
 		    <div class="card-body">
-		      {{ followup.text|process_markup }}
+		      {{ followup.text|automarkup }}
 		    </div>
 		  </div>
 		</li>
diff --git a/helpdesk/templates/helpdesk/ticket_confirm_delete.html b/helpdesk/templates/helpdesk/ticket_confirm_delete.html
index 04fff1513d7db55bce5f64fb7087d568fd001802..ba7859bacaa65c183790ed4cc76b7c10ee82c19e 100644
--- a/helpdesk/templates/helpdesk/ticket_confirm_delete.html
+++ b/helpdesk/templates/helpdesk/ticket_confirm_delete.html
@@ -1,7 +1,6 @@
 {% extends 'helpdesk/base.html' %}
 
 {% load bootstrap %}
-{% load restructuredtext %}
 
 {% block pagetitle %}: Delete Ticket{% endblock pagetitle %}
 
diff --git a/helpdesk/templates/helpdesk/ticket_detail.html b/helpdesk/templates/helpdesk/ticket_detail.html
index 7703fc31819d91e0a8523c16de01755fb128b4a9..dba275b99749d59fc40a425b2dad579182ae337a 100644
--- a/helpdesk/templates/helpdesk/ticket_detail.html
+++ b/helpdesk/templates/helpdesk/ticket_detail.html
@@ -2,7 +2,6 @@
 
 {% load bootstrap %}
 {% load guardian_tags %}
-{% load restructuredtext %}
 
 {% block breadcrumb_items %}
   {{ block.super }}
diff --git a/markup/__init__.py b/markup/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/markup/admin.py b/markup/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e
--- /dev/null
+++ b/markup/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/markup/apps.py b/markup/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..84b7a3fe938f936968f66412ae7459effed3cd28
--- /dev/null
+++ b/markup/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class MarkupConfig(AppConfig):
+    name = 'markup'
diff --git a/markup/constants.py b/markup/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..a6fbbeb90ee48d4276c01928b9639a43a094e317
--- /dev/null
+++ b/markup/constants.py
@@ -0,0 +1,583 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+# Dictionary for regex expressions to recognize reStructuredText headers.
+# This follows the Python conventions: order is #, *, =, -, ", ^ and
+# for the first two levels (# and *), over- and underlining are necessary, while
+# only underlining is needed for the lower four levels. In all cases we
+# require the headline title to be at least one character long, and placed
+# right above the lower headline marker.
+# The regex search should use the re.MULTILINE flag.
+ReST_HEADER_REGEX_DICT = {
+    '#': r'^(#{1,}\n).{1,}\n\1',  # this makes use of a regex backreference
+    '*': r'^(\*{1,}\n).{1,}\n\1', # this makes use of a regex backreference
+    '=': r'^.{1,}\n={1,}\n',   # non-empty line followed by line of =
+    '-': r'^.{1,}\n-{1,}\n',   # non-empty line followed by line of -
+    '"': r'^.{1,}\n"{1,}\n',   # non-empty line followed by line of "
+    '^': r'^.{1,}\n\^{1,}\n'   # non-empty line followed by line of ^
+}
+
+# See list at http://docutils.sourceforge.net/0.4/docs/ref/rst/roles.html
+ReST_ROLES = [
+    "math",
+    "emphasis", "literal", "pep-reference", "rfc-reference",
+    "strong", "subscript", "superscript", "title-reference"
+]
+
+# See list of reStructuredText directives at
+# http://docutils.sourceforge.net/0.4/docs/ref/rst/directives.html
+ReST_DIRECTIVES = [
+    "math",
+    "attention", "caution", "danger", "error", "hint", "important", "note", "tip",
+    "warning", "admonition",
+    "topic", "sidebar", "parsed-literal", "rubric", "epigraph", "highlights",
+    "pull-quote", "compound", "container",
+    "table", "csv-table", "list-table",
+    "contents", "sectnum", "section-autonumbering", "header", "footer",
+    "target-notes",
+    "replace", "unicode", "date", "class", "role", "default-role"
+]
+
+BLEACH_ALLOWED_TAGS = [
+    'a', 'abbr', 'acronym', 'b', 'blockquote', 'br', 'code', 'em',
+    'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'li', 'ol',
+    'p', 'pre', 'strong', 'table', 'td', 'th', 'tr', 'ul'
+]
+
+
+PlainTextSuggestedFormatting = (
+    {
+        'id': 'authorreply',
+        'title': 'Author Reply to Report',
+        'raw':
+r"""
+The referee writes:
+"The authors should extend their exact solution to the two-dimensional case."
+
+Our response:
+Even Bethe did not manage this: see the unfulfilled promise at the end
+of his 1931 paper https://doi.org/10.1007/BF01341708.
+"""
+    },
+)
+
+
+PlainTextSnippets = (
+    {
+        'id': 'maths',
+        'title': 'Maths: inline and displayed',
+        'raw':
+r"""Some say $e^{i\pi} + 1 = 0$ is the most beautiful equation of all.
+
+Do you know this famous Hamiltonian?
+\[
+H = \sum_j {\boldsymbol S}_j \cdot {\boldsymbol S}_{j+1}
+\]
+
+What about this one?
+$$
+H = \int dx \left[ \partial_x \Psi^\dagger \partial_x \Psi
++ c \Psi^\dagger \Psi^\dagger \Psi \Psi \right]
+$$
+"""
+    },
+
+    {
+        'id': 'maths_multiple_lines',
+        'title': 'Maths: multiple lines',
+        'raw':
+r"""
+Equations on multiple lines:
+\[
+\nabla \cdot {\boldsymbol E} = \frac{\rho}{\epsilon_0}, \qquad
+\nabla \times {\boldsymbol E} + \frac{\partial \boldsymbol B}{\partial t}= 0 \\\\
+\nabla \cdot {\boldsymbol B} = 0, \qquad
+\nabla \times {\boldsymbol B} - \frac{1}{c^2} \frac{\partial \boldsymbol E}{\partial t}
+= \mu_0 {\boldsymbol J}
+\]
+
+\[
+\begin{eqnarray*}
+\nabla \cdot {\boldsymbol E} &=& \frac{\rho}{\epsilon_0}, \qquad
+\nabla \times {\boldsymbol E} + \frac{\partial \boldsymbol B}{\partial t} &=& 0 \\\\
+\nabla \cdot {\boldsymbol B} &=& 0, \qquad
+\nabla \times {\boldsymbol B} - \frac{1}{c^2} \frac{\partial \boldsymbol E}{\partial t}
+&=& \mu_0 {\boldsymbol J}
+\end{eqnarray*}
+\]
+"""
+    }
+)
+
+
+MarkdownSuggestedFormatting = (
+    {
+        'id': 'authorreply',
+        'title': 'Author Reply to Report',
+        'raw':
+r"""
+**The referee writes:**
+> The authors should extend their exact solution to the two-dimensional case.
+
+**Our response:**
+
+Even Bethe did not manage this: see the unfulfilled promise at the end
+of his [1931 paper](https://doi.org/10.1007/BF01341708).
+"""
+    },
+)
+
+
+MarkdownSnippets = (
+    {
+        'id': 'paragraphs',
+        'title': 'Paragraphs and line breaks',
+        'raw':
+"""Including an empty line between two blocks of text separates those into
+
+two different paragraphs.
+
+Typing text on consecutive lines separated
+by linebreaks
+will merge the lines into one
+paragraph.
+
+However if you explicitly end a line with two spaces
+then a linebreak will be forced.""",},
+
+    {
+        'id': 'headlines',
+        'title': 'Headlines',
+        'raw':
+"""# Level 1 (html h1)
+Topmost headline
+
+## Level 2 (h2)
+two
+
+### Level 3 (h3)
+three
+
+#### Level 4 (h4)
+four
+
+##### Level 5 (h5)
+five
+
+###### Level 6 (h6)
+six, lowest level available.""",},
+
+    {
+        'id': 'emphasis',
+        'title': 'Emphasis',
+        'raw':
+"""You can obtain italics with *asterisks* or _single underscores_,
+and boldface using **double asterisks** or __double underscores__.
+
+If you need to explicitly use these characters (namely \* and \_),
+you can escape them with a backslash.""",},
+
+    {
+        'id': 'blockquotes',
+        'title': 'Blockquotes',
+        'raw':
+"""> This is a blockquote with two paragraphs. You should begin
+> each line with a ">" (greater than) symbol.
+>
+> Here is the second paragraph. The same wrapup rules
+> apply as per
+> normal paragraphs.
+
+A line of normal text will separate blockquotes.
+
+> Otherwise
+
+> multiple blockquotes
+
+> will be merged into one.
+
+You can be lazy and simply put a single ">" in front of
+a hard-wrapped paragraph, and all the
+text will be included in a single wrapped-up blockquote.
+
+> This is especially handy if you are copy and pasting e.g.
+a referee's comment which you want to respond to, and
+which spans
+multiple
+lines.
+
+Finally,
+
+> you can
+>> nest
+>>> many levels
+>>>> of blockquotes
+
+>>> and come back to a lower level by putting a blank line
+
+>> to indicate a "terminated" level.
+""",},
+
+    {
+        'id': 'lists',
+        'title': 'Lists',
+        'raw':
+"""Markdown supports unordered (bulleted) and ordered (numbered) lists.
+
+Unordered list items are marked with asterisk, plus or hyphen, which
+can be used interchangeable (even within a single list):
+
+* first item
++ second item
+- third item
+
+Ordered list items are marked by a number (it does not matter which,
+they will be automatically recomputed anyway) followed by a period:
+
+1. first item
+7. second item
+123. third item
+42. fourth item
+1. fifth item
+
+If you separate list items by blank lines, an HTML paragraph will
+be wrapped around each item, giving more padding around the items:
+
+1. first item
+
+1. second item
+
+Nested lists can be obtained by four-space (or tab) indentation:
+
+1. First mainlist item
+    1. first sublist item
+    1. second sublist item
+1. Second mainlist item""",},
+
+    {
+        'id': 'code',
+        'title': 'Code',
+        'raw':
+"""An inline code span, to mention simple things like the
+`print()` function, is obtained by wrapping it with single backticks.
+
+A code block is obtained by indenting the code by 4 spaces or a tab:
+
+    from django import forms
+
+    class MarkupTextForm(forms.Form):
+        markup_text = forms.CharField()
+
+        def get_processed_markup(self):
+            text = self.cleaned_data['markup_text']""",},
+
+    {
+        'id': 'hr',
+        'title': 'Horizontal rules',
+        'raw':
+"""A horizontal rule (HTML hr) can be obtained by writing three or
+more asterisks, hyphens or underscores on a line by themselves:
+
+text
+
+***
+
+more text
+
+-----
+
+more text
+_______
+
+Be careful to include an empty line before hyphens,
+otherwise the system might
+
+confuse the preceding text for a headline
+-------
+""",},
+
+    {
+        'id': 'links',
+        'title': 'Links',
+        'raw':
+"""### Inline-style hyperlinks
+
+Here is an example of an inline link to the [SciPost homepage](https://scipost.org/).
+Please always use the full protocol in the URL
+(so https://scipost.org instead of just scipost.org).
+
+For example, one can also link to
+a specific [Submission](https://scipost.org/submissions/1509.04230v5/) or
+a specific [Report](https://scipost.org/submissions/1509.04230v4/#report_2).
+
+### Reference-style hyperlinks
+
+You can also use reference-style links when citing this [resource][md],
+which you need to cite [again][md] and [again][md] and [again][md].
+
+The reference will be resolved provided you define the link label somewhere in your text.
+
+[md]: https://daringfireball.net/projects/markdown/syntax""",},
+
+    {
+        'id': 'mathematics',
+        'title': 'Mathematics',
+        'raw':
+r"""### Inline maths
+
+You can have simple inline equations like this: $E = mc^2$ by enclosing them with
+dollar signs.
+
+### Displayed maths
+
+For displayed maths, you need to enclose the equations with escaped square brackets
+
+\[
+H = \sum_j {\boldsymbol S}_j \cdot {\boldsymbol S}_{j+1}
+\]
+
+or with double dollar signs (though it works, this is less good,
+since beginning and end markers are not distinguishable):
+
+$$
+H = \sum_j S^x_j S^x_{j+1} + S^y_j S^y_{j+1} + \Delta S^z_j S^z_{j+1}
+$$
+
+Multiline equations can be obtained by using the ``\\`` carriage return as usual;
+to align your equations, use the ``align`` environment. For example:
+
+\[
+\begin{align*}
+\nabla \cdot {\boldsymbol E} &= \frac{\rho}{\epsilon_0}, &
+\nabla \times {\boldsymbol E} + \frac{\partial \boldsymbol B}{\partial t} &= 0, \\
+\nabla \cdot {\boldsymbol B} &= 0, &
+\nabla \times {\boldsymbol B} - \frac{1}{c^2} \frac{\partial \boldsymbol E}{\partial t}
+&= \mu_0 {\boldsymbol J}
+\end{align*}
+\]
+"""},
+)
+
+
+ReStructuredTextSnippets = (
+    {
+        'id': 'paragraphs',
+        'title': 'Paragraphs and line breaks',
+        'raw':
+"""Including an empty line between two blocks of text separates those into
+
+two different paragraphs.
+
+Typing text on consecutive lines separated
+by linebreaks
+will merge the lines into one
+paragraph.
+
+As in Python, indentation is significant, so lines of a paragraph have to be
+indented to the same level.""",},
+
+    {
+        'id': 'headlines',
+        'title': 'Headlines',
+        'raw':
+"""##################
+Level 1 (html h1)
+##################
+
+Topmost headline
+
+*****************
+Level 2 (h2)
+*****************
+
+two
+
+Level 3 (h3)
+==================
+
+three
+
+Level 4 (h4)
+-------------------
+
+four
+
+Level 5 (h5)
+\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"
+
+five
+
+Level 6 (h6)
+^^^^^^^^^^^^^^^^^^^
+
+six, lowest level available.""",},
+
+    {
+        'id': 'emphasis',
+        'title': 'Emphasis',
+        'raw':
+"""You can obtain italics with *asterisks*,
+boldface using **double asterisks** and code samples using ``double backquotes``.
+Note that these cannot be nested, and that there must not be a space at the
+start or end of the contents.
+
+If you need to explicitly use these characters (namely \*),
+you can escape them with a backslash.""",},
+
+    {
+        'id': 'blockquotes',
+        'title': 'Blockquotes',
+        'raw':
+"""It is often handy to use blockquotes.
+
+ This is a blockquote with two paragraphs, obtained by simple
+ indentation from the surrounding text. For multiple lines,
+ each line should be indented the same.
+
+ Here is the second paragraph, with lines indented
+ to the same level as the previous ones to preserve the
+ blockquote.
+
+To preserve line breaks, you can use line blocks:
+
+| Here is
+| a small paragraph
+| with linebreaks preserved.
+
+""",},
+
+    {
+        'id': 'lists',
+        'title': 'Lists',
+        'raw':
+"""reStructuredText supports unordered (bulleted) and ordered (numbered) lists.
+
+Unordered list items are marked with asterisk:
+
+* first item
+* second item
+* third item
+
+Ordered list items are marked by a number or # followed by a period:
+
+1. first item
+2. second item
+3. third item
+
+
+Nested lists can be obtained by indentation:
+
+* First mainlist item
+
+  * first sublist item
+  * second sublist item
+* Second mainlist item
+
+
+There are also *definition lists* obtained like this:
+
+term (up to a line of text)
+   Definition of the term, which must be indented
+
+   and can even consist of multiple paragraphs
+
+next term
+   Description.
+""",},
+
+    {
+        'id': 'code',
+        'title': 'Code',
+        'raw':
+"""An inline code span, to mention simple things like the
+``print()`` function, is obtained by wrapping it with double backticks.
+
+A code block is obtained by the ``::`` marker followed by the indented code::
+
+    from django import forms
+
+    class MarkupTextForm(forms.Form):
+        markup_text = forms.CharField()
+
+        def get_processed_markup(self):
+            text = self.cleaned_data['markup_text']
+
+which can then be followed by normal text.""",},
+
+    {
+        'id': 'tables',
+        'title': 'Tables',
+        'raw':
+"""
+A grid table can be written by "painting" it directly:
+
++------------------------+------------+----------+----------+
+| Header row, column 1   | Header 2   | Header 3 | Header 4 |
+| (header rows optional) |            |          |          |
++========================+============+==========+==========+
+| body row 1, column 1   | column 2   | column 3 | column 4 |
++------------------------+------------+----------+----------+
+| body row 2             | ...        | ...      |          |
++------------------------+------------+----------+----------+
+
+""",},
+
+    {
+        'id': 'links',
+        'title': 'Links',
+        'raw':
+"""Here is an example of an inline link to the `SciPost homepage <https://scipost.org/>`_.
+
+For example, one can also link to
+a specific `Submission <https://scipost.org/submissions/1509.04230v5/>`_
+or a specific `Report <https://scipost.org/submissions/1509.04230v4/#report_2>`_.
+
+You can also use reference-style links when citing this `resource`_, the reference
+will be resolved provided you define the link label somewhere
+in your text.
+
+.. _resource: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html""",},
+
+    {
+        'id': 'mathematics',
+        'title': 'Mathematics',
+        'raw':
+r"""
+For simple inline equations, use the :code:`math` role like this: :math:`E = mc^2`.
+
+For displayed maths, the :code:`math` directive must be used:
+
+.. math::
+  H = \sum_j {\boldsymbol S}_j \cdot {\boldsymbol S}_{j+1}
+
+Multiline equations can be obtained by using the ``\\`` carriage return as usual:
+
+.. math::
+  &\nabla \cdot {\boldsymbol E} = \frac{\rho}{\epsilon_0},
+  &\nabla \times {\boldsymbol E} + \frac{\partial \boldsymbol B}{\partial t} = 0, \\
+  &\nabla \cdot {\boldsymbol B} = 0,
+  &\nabla \times {\boldsymbol B} - \frac{1}{c^2} \frac{\partial \boldsymbol E}{\partial t}
+  = \mu_0 {\boldsymbol J}
+
+"""},
+)
+
+
+ReSTSuggestedFormatting = (
+    {
+        'id': 'authorreply',
+        'title': 'Author Reply to Report',
+        'raw':
+r"""
+**The referee writes:**
+
+  The authors should extend their exact solution to the two-dimensional case.
+
+**Our response:**
+
+Even Bethe did not manage this: see the unfulfilled promise at the end
+of his `1931 paper <https://doi.org/10.1007/BF01341708>`_ .
+"""
+    },
+)
diff --git a/markup/forms.py b/markup/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa84f88c794116f39d9f4ccde1456a6e21119396
--- /dev/null
+++ b/markup/forms.py
@@ -0,0 +1,14 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django import forms
+
+from .utils import process_markup
+
+
+class MarkupTextForm(forms.Form):
+    markup_text = forms.CharField()
+
+    def get_processed_markup(self):
+        return process_markup(self.cleaned_data['markup_text'])
diff --git a/markup/migrations/__init__.py b/markup/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/markup/models.py b/markup/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91
--- /dev/null
+++ b/markup/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/markup/templates/markup/base.html b/markup/templates/markup/base.html
new file mode 100644
index 0000000000000000000000000000000000000000..1e4b463f7f188bdf37226fd71f33f69bd3ffac30
--- /dev/null
+++ b/markup/templates/markup/base.html
@@ -0,0 +1,13 @@
+{% extends 'scipost/base.html' %}
+
+{% block breadcrumb %}
+  <div class="container-outside header">
+    <div class="container">
+      <nav class="breadcrumb hidden-sm-down">
+        {% block breadcrumb_items %}
+          <a href="{% url 'markup:help' %}" class="breadcrumb-item">Markup</a>
+        {% endblock %}
+      </nav>
+    </div>
+  </div>
+{% endblock %}
diff --git a/markup/templates/markup/help.html b/markup/templates/markup/help.html
new file mode 100644
index 0000000000000000000000000000000000000000..d5656db56bdd0967b99ad1cb108e599c6331ed9a
--- /dev/null
+++ b/markup/templates/markup/help.html
@@ -0,0 +1,170 @@
+{% extends 'markup/base.html' %}
+
+{% block pagetitle %}: Markup help{% endblock pagetitle %}
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item">Help</span>
+{% endblock %}
+
+{% load automarkup %}
+
+{% block content %}
+
+  <div class="row">
+    <div class="col-12">
+      <h2 class="highlight">Markup help</h2>
+
+      <p>
+	On many occasions while contributing to SciPost,
+	one has to fill in a text-based field in a web form.
+	For many reasons, it is desirable to give <em>structure</em> to what one writes.
+	We offer <strong>markup facilities</strong> to enable you to do this.
+      </p>
+
+      <p>
+	We support three options:
+	<ul>
+	  <li>plain text</li>
+	  <li>Markdown</li>
+	  <li>reStructuredText</li>
+	</ul>
+	Each of these supports LaTeX via <a href="https://www.mathjax.org">MathJax</a>,
+	though the precise semantics varies from one option to the other.
+      </p>
+
+      <p>
+	Which should you choose? This is mostly a question of personal preference.
+	To help you decide, here is a quick summary of main points to bear in mind:
+	<table class="table table-bordered">
+	  <tr>
+	    <th>Option</th>
+	    <th>Advantages</th>
+	    <th>Disadvantages</th>
+	    <th>Recommended for</th>
+	  </tr>
+	  <tr>
+	    <td>Plain text</td>
+	    <td>
+	      <ul>
+		<li>Simplicity</li>
+	      </ul>
+	    </td>
+	    <td>
+	      <ul>
+		<li>No markup!</li>
+	      </ul>
+	    </td>
+	    <td>
+	      Anybody not wanting to bother with markup
+	    </td>
+	  </tr>
+	  <tr>
+	    <td>Markdown</td>
+	    <td>
+	      <ul>
+		<li>ease of use</li>
+		<li>purposefully simple</li>
+		<li>
+		  provides more or less all the markup you'll ever need,
+		  at least for small, simple snippets
+		</li>
+	      </ul>
+	    </td>
+	    <td>
+	      <ul>
+		<li>non-standardized: many dozen "dialects" exist</li>
+		<li>facilities for some simple things (<em>e.g.</em> tables) are missing</li>
+		<li>not really meant for large, complex documents</li>
+	      </ul>
+	    </td>
+	    <td>
+	      Everybody wishing for or requiring markup
+	    </td>
+	  </tr>
+	  <tr>
+	    <td>reStructuredText</td>
+	    <td>
+	      <ul>
+		<li>relative ease of use</li>
+		<li>standardized: the language is well-defined and stable</li>
+		<li>
+		  it's the standard documentation language for Python;
+		  <a href="http://www.sphinx-doc.org/">Sphinx</a> uses ReST
+		  files as input
+		</li>
+		<li>ReST files are easily exportable to other formats such
+		  as HTML, LaTeX etc.
+		</li>
+	      </ul>
+	    </td>
+	    <td>
+	      <ul>
+		<li>support for maths is good, but remains less extensive than in LaTeX,
+		  and requires annoying indentation in multiline directives</li>
+	      </ul>
+	    </td>
+	    <td>
+	      Pythonistas and Sphinx aficionados
+	    </td>
+	  </tr>
+	</table>
+      </p>
+
+      <p>
+	Which option you choose is completely up to you. Our system will automatically
+	determine which one you are using and render your input accordingly.
+      </p>
+
+      <br>
+
+      <h2 class="highlight" id="Plain">Plain text&emsp;
+	<em><small><i class="fa fa-arrow-right"></i> See our <a href="{% url 'markup:plaintext_help' %}">plain text-specific</a> help page</small></em>
+      </h2>
+      {% for suggestion in PlainTextSuggestions %}
+	<h4 id="{{ suggestion.id }}">Example: {{ suggestion.title }}</h4>
+	<div class="row">
+	  <div class="col-6">
+	    <pre>{{ suggestion.raw }}</pre>
+	  </div>
+	  <div class="col-6">
+	    {{ suggestion.raw|automarkup }}
+	  </div>
+	</div>
+      {% endfor %}
+
+      <br>
+
+      <h2 class="highlight" id="Markdown">Markdown&emsp;
+	<em><small><i class="fa fa-arrow-right"></i> See our <a href="{% url 'markup:markdown_help' %}">Markdown-specific</a> help page</small></em></h2>
+	{% for suggestion in MarkdownSuggestions %}
+	  <h4 id="{{ suggestion.id }}">Example: {{ suggestion.title }}</h4>
+	  <div class="row">
+	    <div class="col-6">
+	      <pre>{{ suggestion.raw }}</pre>
+	    </div>
+	    <div class="col-6">
+	      {{ suggestion.raw|automarkup }}
+	    </div>
+	  </div>
+	{% endfor %}
+
+	<br>
+
+	<h2 class="highlight" id="reStructuredText">reStructuredText&emsp;
+	  <em><small><i class="fa fa-arrow-right"></i> See our <a href="{% url 'markup:restructuredtext_help' %}">reStructuredText-specific</a> help page</small></em></h2>
+      {% for suggestion in ReSTSuggestions %}
+	<h4 id="{{ suggestion.id }}">Example: {{ suggestion.title }}</h4>
+	<div class="row">
+	  <div class="col-6">
+	    <pre>{{ suggestion.raw }}</pre>
+	  </div>
+	  <div class="col-6">
+	    {{ suggestion.raw|automarkup }}
+	  </div>
+	</div>
+      {% endfor %}
+    </div>
+  </div>
+
+{% endblock content %}
diff --git a/markup/templates/markup/markdown_help.html b/markup/templates/markup/markdown_help.html
new file mode 100644
index 0000000000000000000000000000000000000000..a5211f716156d850e6678eaae4195f1afa2d0f8c
--- /dev/null
+++ b/markup/templates/markup/markdown_help.html
@@ -0,0 +1,46 @@
+{% extends 'markup/base.html' %}
+
+{% block pagetitle %}: Markup help{% endblock pagetitle %}
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item"><a href="{% url 'markup:help' %}">Help</a></span>
+    <span class="breadcrumb-item">Markdown</span>
+{% endblock %}
+
+{% load automarkup %}
+
+{% block content %}
+
+  <div class="row">
+    <div class="col-12">
+      <h2 class="highlight">Markdown help</h2>
+
+      <p>You will find below a quick summary of Markdown basics, as enabled here at SciPost.</p>
+      <p>You can find more details about Markdown's syntax at <a href="https://daringfireball.net/projects/markdown/syntax">this page</a>.</p>
+
+      <h3>Quick links</h3>
+      <ul>
+	{% for snippet in snippets %}
+	  <li><a href="#{{ snippet.id }}">{{ snippet.title }}</a></li>
+	{% endfor %}
+      </ul>
+
+      {% for snippet in snippets %}
+	<h3 class="highlight" id="{{ snippet.id }}">{{ snippet.title }}</h3>
+	<div class="row">
+	  <div class="col-6">
+	    <h3><strong>If you write:</strong></h3>
+	    <pre>{{ snippet.raw }}</pre>
+	  </div>
+	  <div class="col-6">
+	    <h3><strong>You will get:</strong></h3>
+	    {{ snippet.raw|automarkup:'Markdown' }}
+	  </div>
+	</div>
+      {% endfor %}
+
+    </div>
+  </div>
+
+{% endblock content %}
diff --git a/markup/templates/markup/plaintext_help.html b/markup/templates/markup/plaintext_help.html
new file mode 100644
index 0000000000000000000000000000000000000000..b18ae6f588d84347b98bd6e385dc6af810a9c434
--- /dev/null
+++ b/markup/templates/markup/plaintext_help.html
@@ -0,0 +1,49 @@
+{% extends 'markup/base.html' %}
+
+{% block pagetitle %}: Markup help{% endblock pagetitle %}
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item"><a href="{% url 'markup:help' %}">Help</a></span>
+  <span class="breadcrumb-item">Plain text</span>
+{% endblock %}
+
+{% load automarkup %}
+
+{% block content %}
+
+  <div class="row">
+    <div class="col-12">
+      <h2 class="highlight">Plain text help</h2>
+      <p>
+	Plain text is the most straightforward format to use in our text fields,
+	but this comes at the cost of having no markup facilities.
+
+	Mathematics (both online and displayed) are enabled via MathJax.
+	You will find some simple examples below.
+      </p>
+      <h3>Quick links</h3>
+      <ul>
+	{% for snippet in snippets %}
+	  <li><a href="#{{ snippet.id }}">{{ snippet.title }}</a></li>
+	{% endfor %}
+      </ul>
+
+      {% for snippet in snippets %}
+	<h3 class="highlight" id="{{ snippet.id }}">{{ snippet.title }}</h3>
+	<div class="row">
+	  <div class="col-6">
+	    <h3><strong>If you write:</strong></h3>
+	    <pre>{{ snippet.raw }}</pre>
+	  </div>
+	  <div class="col-6">
+	    <h3><strong>You will get:</strong></h3>
+	    {{ snippet.raw|automarkup:'Markdown' }}
+	  </div>
+	</div>
+      {% endfor %}
+
+    </div>
+  </div>
+
+{% endblock content %}
diff --git a/markup/templates/markup/restructuredtext_help.html b/markup/templates/markup/restructuredtext_help.html
new file mode 100644
index 0000000000000000000000000000000000000000..e6c4fcb272553bf050593b9a9aa4412f9bf7513d
--- /dev/null
+++ b/markup/templates/markup/restructuredtext_help.html
@@ -0,0 +1,48 @@
+{% extends 'markup/base.html' %}
+
+{% block pagetitle %}: Markup help{% endblock pagetitle %}
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item"><a href="{% url 'markup:help' %}">Help</a></span>
+  <span class="breadcrumb-item">reStructuredText</span>
+{% endblock %}
+
+{% load automarkup %}
+
+{% block content %}
+
+  <div class="row">
+    <div class="col-12">
+      <h2 class="highlight">reStructuredText help</h2>
+
+      <p>You will find below a quick summary of reStructuredText basics,
+	as enabled here at SciPost.</p>
+      <p>You can find more details about reStructuredText's syntax
+	for example at <a href="https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html">this page</a>.</p>
+
+      <h3>Quick links</h3>
+      <ul>
+	{% for snippet in snippets %}
+	  <li><a href="#{{ snippet.id }}">{{ snippet.title }}</a></li>
+	{% endfor %}
+      </ul>
+
+      {% for snippet in snippets %}
+	<h3 class="highlight" id="{{ snippet.id }}">{{ snippet.title }}</h3>
+	<div class="row">
+	  <div class="col-6">
+	    <h3><strong>If you write:</strong></h3>
+	    <pre>{{ snippet.raw }}</pre>
+	  </div>
+	  <div class="col-6">
+	    <h3><strong>You will get:</strong></h3>
+	    {{ snippet.raw|automarkup:'reStructuredText' }}
+	  </div>
+	</div>
+      {% endfor %}
+
+    </div>
+  </div>
+
+{% endblock content %}
diff --git a/markup/templatetags/__init__.py b/markup/templatetags/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/markup/templatetags/automarkup.py b/markup/templatetags/automarkup.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce5cb078bb282374cb9b6066b5c0b430aafa1ce3
--- /dev/null
+++ b/markup/templatetags/automarkup.py
@@ -0,0 +1,15 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django import template
+
+from ..utils import process_markup
+
+
+register = template.Library()
+
+
+@register.filter(name='automarkup')
+def automarkup(text, language_forced=None):
+    return process_markup(text, language_forced)['processed']
diff --git a/markup/tests.py b/markup/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6
--- /dev/null
+++ b/markup/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/markup/urls.py b/markup/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd40e75981328843a434980e57144cd64c8c689e
--- /dev/null
+++ b/markup/urls.py
@@ -0,0 +1,38 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.conf.urls import url
+
+from . import views
+
+app_name = 'markup'
+
+urlpatterns = [
+
+    url(
+        r'^process/$',
+        views.process,
+        name='process'
+    ),
+    url(
+        r'^help/$',
+        views.markup_help,
+        name='help'
+    ),
+    url(
+        r'^help/plaintext$',
+        views.plaintext_help,
+        name='plaintext_help'
+    ),
+    url(
+        r'^help/Markdown$',
+        views.markdown_help,
+        name='markdown_help'
+    ),
+    url(
+        r'^help/reStructuredText$',
+        views.restructuredtext_help,
+        name='restructuredtext_help'
+    ),
+]
diff --git a/markup/utils.py b/markup/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1cc33ac557979de48d15b80778bbcc3feae691e
--- /dev/null
+++ b/markup/utils.py
@@ -0,0 +1,355 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+import bleach
+from docutils.core import publish_parts
+import markdown
+from io import StringIO
+import re
+
+from django.template.defaultfilters import linebreaksbr
+from django.utils.encoding import force_text
+from django.utils.safestring import mark_safe
+
+from .constants import ReST_HEADER_REGEX_DICT, ReST_ROLES, ReST_DIRECTIVES, BLEACH_ALLOWED_TAGS
+
+
+# Inline or displayed math
+def match_inline_math(text):
+    """Return first match object of regex search for inline math ``$...$`` or ``\(...\)``."""
+    match = re.search(r'\$[^$]+\$', text)
+    if match:
+        return match
+    return re.search(r'\\\(.+\\\)', text)
+
+def match_displayed_math(text):
+    """Return first match object of regex search for displayed math ``$$...$$`` or ``\[...\]``."""
+    match = re.search(r'\$\$.+\$\$', text, re.DOTALL)
+    if match:
+        return match
+    return re.search(r'\\\[.+\\\]', text, re.DOTALL)
+
+
+# Markdown
+def match_md_header(text, level=None):
+    """
+    Return first match object of regex search for Markdown headers in form #{level,}.
+
+    If not level is given, all levels 1 to 6 are checked, returning the first match or None.
+    """
+    if not level:
+        for newlevel in range(1, 7):
+            match = match_md_header(text, newlevel)
+            if match:
+                return match
+        return None
+    if not isinstance(level, int):
+        raise TypeError('level must be an int')
+    if level < 1 or level > 6:
+        raise ValueError('level must be an integer from 1 to 6')
+    return re.search(r'^#{' + str(level) + ',}[ ].+$', text, re.MULTILINE)
+
+def match_md_blockquote(text):
+    """Return first match of regex search for Markdown blockquote."""
+    return re.search(r'(^[ ]*>[ ].+){1,5}', text, re.DOTALL | re.MULTILINE)
+
+def match_md_hyperlink_inline(text):
+    """Return first match of regex search for Markdown inline hyperlink."""
+    return re.search(r'\[.+\]\(http.+\)', text)
+
+def match_md_hyperlink_reference(text):
+    """Return first match of regex search for Markdown reference-style hyperlink."""
+    return re.search(r'\[.+\]: http.+', text)
+
+
+# reStructuredText
+def match_rst_role(text, role=None):
+    """
+    Return first match object of regex search for given ReST role :role:`... .
+
+    If no role is given, all roles in ReST_ROLES are tested one by one.
+    """
+    if not role:
+        for newrole in ReST_ROLES:
+            match = match_rst_role(text, newrole)
+            if match:
+                return match
+        return None
+    if role not in ReST_ROLES:
+        raise ValueError('this role is not listed in ReST roles')
+    return re.search(r':' + role + ':`.+`', text)
+
+def match_rst_directive(text, directive=None):
+    """
+    Return first match object of regex search for given ReST directive.
+
+    If no directive is given, all directives in ReST_DIRECTIVES are tested one by one.
+
+    The first one to three lines after the directive statement are also captured.
+    """
+    if not directive:
+        for newdirective in ReST_DIRECTIVES:
+            match = match_rst_directive(text, newdirective)
+            if match:
+                return match
+        return None
+    if directive not in ReST_DIRECTIVES:
+        raise ValueError('this directive is not listed in ReST directives')
+    return re.search(r'^\.\. ' + directive + '::(.+)*(\n(.+)*){1,3}', text, re.MULTILINE)
+
+def match_rst_header(text, symbol=None):
+    """
+    Return first match object of regex search for reStructuredText header.
+
+    Python conventions are followed, namely that ``#`` and ``*`` headers have
+    both over and underline (of equal length, so faulty ones are not matched),
+    while the others (``=``, ``-``, ``"`` and ``^``) only have the underline.
+    """
+    if not symbol:
+        for newsymbol in ['#', '*', '=', '-', '"', '^']:
+            match = match_rst_header(text, newsymbol)
+            if match:
+                return match
+        return None
+    if symbol not in ReST_HEADER_REGEX_DICT.keys():
+        raise ValueError('symbol is not a ReST header symbol')
+    return re.search(ReST_HEADER_REGEX_DICT[symbol], text, re.MULTILINE)
+
+def match_rst_hyperlink_inline(text):
+    """Return first match of regex search for reStructuredText inline hyperlink."""
+    return re.search(r'`.+<http.+>`_', text)
+
+def match_rst_hyperlink_reference(text):
+    """Return first match of regex search for reStructuredText reference-style hyperlink."""
+    # The match must not start with `_ (end of previous hyperlink) or contain
+    # a < (it's then assumed to be an inline hyperlink with <http...).
+    return re.search(r'`[^_][^<]+`_', text)
+
+
+def check_markers(markers):
+    """
+    Checks the consistency of a markers dictionary. Returns a detector.
+    """
+    markers_cut = {}
+    for key, val in markers.items():
+        markers_cut[key] = {}
+        for key2, val2 in val.items():
+            if val2:
+                markers_cut[key][key2] = val2
+    print('markers:\n%s' % markers)
+    print('markers_cut:\n%s' % markers_cut)
+
+    if len(markers_cut['rst']) > 0:
+        if len(markers_cut['md']) > 0:
+            return {
+                'language': 'plain',
+                'errors': ('Inconsistency: Markdown and reStructuredText syntaxes are mixed:\n\n'
+                           'Markdown: %s\n\nreStructuredText: %s' % (
+                               markers_cut['md'].popitem(),
+                               markers_cut['rst'].popitem()))
+            }
+        elif len(markers_cut['plain_or_md']) > 0:
+            return {
+                'language': 'plain',
+                'errors': ('Inconsistency: plain/Markdown and reStructuredText '
+                           'syntaxes are mixed:\n\n'
+                           'Markdown: %s\n\nreStructuredText: %s' % (
+                               markers_cut['plain_or_md'].popitem(),
+                               markers_cut['rst'].popitem()))
+            }
+        return {
+            'language': 'reStructuredText',
+            'errors': None,
+        }
+
+    elif len(markers_cut['md']) > 0:
+        return {
+            'language': 'Markdown',
+            'errors': None,
+        }
+
+    return {
+        'language': 'plain',
+        'errors': None,
+    }
+
+
+def detect_markup_language(text):
+    """
+    Detect whether text is plain text, Markdown or reStructuredText.
+
+    This method returns a dictionary containing:
+    * language
+    * errors
+
+    Inline and displayed maths are assumed enabled through MathJax.
+    For plain text and Markdown, this assumes the conventions
+    * inline: $ ... $ and \( ... \)
+    * displayed: $$ ... $$ and \[ ... \]
+
+    while for reStructuredText, the ``math`` role and directive are used.
+
+    We define markers, and indicator. A marker is a regex which occurs
+    in only one of the languages. An indicator occurs in more than one,
+    but not all languages.
+
+    Language markers:
+
+    Markdown:
+    * headers: [one or more #] [non-empty text]
+    * blockquotes: one or more lines starting with > [non-empty text]
+
+    reStructuredText:
+    * use of the :math: role or .. math: directive
+    * [two or more #][blank space][carriage return]
+      [text on a single line, as long as or shorter than # sequence]
+      [same length of #]
+    * same thing but for * headlines
+    * other header markers (=, -, \" and \^)
+    * use of any other role
+    * use of any other directive
+
+    Language indicators:
+
+    Plain text or Markdown:
+    * inline or displayed maths
+
+    Markdown or reStructuredText:
+    * [=]+ alone on a line  <- users discouraged to use this in Markdown
+    * [-]+ alone on a line  <- users discouraged to use this in Markdown
+
+    Exclusions (sources of errors):
+    * inline or displayed maths cannot be used in ReST
+
+    Any simultaneously present markers to two different languages
+    return an error.
+
+    Checking order:
+    * maths
+    * headers/blockquotes
+    * hyperlinks
+    * rst roles
+    * rst directives
+    """
+
+    markers = {
+        'plain_or_md': {},
+        'md': {},
+        'rst': {},
+    }
+
+    # Step 1: check maths
+    # Inline maths is of the form $ ... $ or \( ... \)
+    markers['plain_or_md']['inline_math'] = match_inline_math(text)
+    # Displayed maths is of the form \[ ... \] or $$ ... $$
+    markers['plain_or_md']['displayed_math'] = match_displayed_math(text)
+    # For rst, check math role and directive
+    markers['rst']['math_role'] = match_rst_role(text, 'math')
+    markers['rst']['math_directive'] = match_rst_directive(text, 'math')
+
+    # Step 2: check headers and blockquotes
+    markers['md']['header'] = match_md_header(text)
+    markers['md']['blockquote'] = match_md_blockquote(text)
+    markers['rst']['header'] = match_rst_header(text)
+
+    # Hyperrefs
+    markers['md']['href_inline'] = match_md_hyperlink_inline(text)
+    markers['md']['href_reference'] = match_md_hyperlink_reference(text)
+    markers['rst']['href_inline'] = match_rst_hyperlink_inline(text)
+    markers['rst']['href_reference'] = match_rst_hyperlink_reference(text)
+
+    # ReST roles and directives
+    markers['rst']['role'] = match_rst_role(text)
+    markers['rst']['directive'] = match_rst_directive(text)
+
+    detector = check_markers(markers)
+    return detector
+
+
+def apply_markdown_preserving_displayed_maths_bracket(text):
+    """
+    Subsidiary function called by ``apply_markdown_preserving_displayed_maths``.
+    See explanations in docstring of that method.
+    """
+    part = text.partition(r'\[')
+    part2 = part[2].partition(r'\]')
+    return '%s%s%s%s%s' % (
+        markdown.markdown(part[0], output_format='html5'),
+        part[1],
+        part2[0],
+        part2[1],
+        apply_markdown_preserving_displayed_maths_bracket(part2[2]) if len(part2[2]) > 0 else '')
+
+def apply_markdown_preserving_displayed_maths(text):
+    """
+    Processes the string text by first splitting out displayed maths, then applying
+    Markdown on the non-displayed math parts.
+
+    Both ``$$ ... $$`` and ``\[ ... \]`` are recognized, so a double recursive logic is used,
+    first dealing with the ``$$ ... $$`` and then with the ``\[ .. \]``.
+    See the complementary method ``apply_markdown_preserving_displayed_maths_bracket``.
+    """
+    part = text.partition('$$')
+    part2 = part[2].partition('$$')
+    return '%s%s%s%s%s' % (
+        apply_markdown_preserving_displayed_maths_bracket(part[0]),
+        part[1],
+        part2[0],
+        part2[1],
+        apply_markdown_preserving_displayed_maths(part2[2]) if len(part2[2]) > 0 else '')
+
+
+def process_markup(text, language_forced=None):
+
+    markup_detector = detect_markup_language(text)
+
+    markup = {
+        'language': 'plain',
+        'errors': None,
+        'warnings': None,
+        'processed': ''
+    }
+
+    if language_forced and language_forced != markup_detector['language']:
+        markup['warnings'] = (
+            'Warning: markup language was forced to %s, while the detected one was %s.'
+            ) % (language_forced, markup_detector['language'])
+
+    language = language_forced if language_forced else markup_detector['language']
+    markup['language'] = language
+    markup['errors'] = markup_detector['errors']
+
+    if markup['errors']:
+        return markup
+
+    if language == 'reStructuredText':
+        warnStream = StringIO()
+        try:
+            parts = publish_parts(
+                source=text,
+                writer_name='html5_polyglot',
+                settings_overrides={
+                    'math_output': 'MathJax  https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML,Safe',
+                    'initial_header_level': 1,
+                    'doctitle_xform': False,
+                    'raw_enabled': False,
+                    'file_insertion_enabled': False,
+                    'warning_stream': warnStream
+                })
+            markup['processed'] = mark_safe(force_text(parts['html_body']))
+        except:
+            markup['errors'] = warnStream.getvalue()
+
+    elif language == 'Markdown':
+        markup['processed'] = mark_safe(
+            bleach.clean(
+                apply_markdown_preserving_displayed_maths(text),
+                tags=BLEACH_ALLOWED_TAGS
+            )
+        )
+
+    else:
+        markup['processed'] = linebreaksbr(text)
+
+    return markup
diff --git a/markup/views.py b/markup/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..8fb854523a6fe700963729a1b382d434ca066577
--- /dev/null
+++ b/markup/views.py
@@ -0,0 +1,71 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.contrib.auth.decorators import login_required
+from django.http import JsonResponse
+from django.shortcuts import render
+
+from .constants import (PlainTextSuggestedFormatting, PlainTextSnippets,
+                        MarkdownSuggestedFormatting, MarkdownSnippets,
+                        ReSTSuggestedFormatting, ReStructuredTextSnippets)
+from .forms import MarkupTextForm
+
+
+@login_required
+def process(request):
+    """
+    API call to process the POSTed text.
+
+    This returns a JSON dict containing
+
+    * language
+    * processed_markup
+    """
+    form = MarkupTextForm(request.POST or None)
+    if form.is_valid():
+        print('response: \n%s' % form.get_processed_markup())
+        return JsonResponse(form.get_processed_markup())
+    return JsonResponse({})
+
+
+def markup_help(request):
+    """
+    General help page about markup facilities at SciPost.
+    """
+    context = {
+        'PlainTextSuggestions': PlainTextSuggestedFormatting,
+        'MarkdownSuggestions': MarkdownSuggestedFormatting,
+        'ReSTSuggestions': ReSTSuggestedFormatting,
+    }
+    return render(request, 'markup/help.html', context)
+
+
+def plaintext_help(request):
+    """
+    Help page for plain text.
+    """
+    context = {
+        'snippets': PlainTextSnippets,
+    }
+    return render(request, 'markup/plaintext_help.html', context)
+
+def markdown_help(request):
+    """
+    Help page for Markdown.
+    """
+    context = {
+        'suggestions': MarkdownSuggestedFormatting,
+        'snippets': MarkdownSnippets,
+    }
+    return render(request, 'markup/markdown_help.html', context)
+
+
+def restructuredtext_help(request):
+    """
+    Help page for reStructuredText.
+    """
+    context = {
+        'snippets': ReStructuredTextSnippets,
+    }
+    return render(request, 'markup/restructuredtext_help.html', context)
diff --git a/requirements.txt b/requirements.txt
index f0e3512c948439b3e6232c46f014fe89135ffc11..0707b2b595a1cb1730575a2f75810ee37505ca7d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -57,12 +57,12 @@ html2text
 # Mongo (Metacore)
 mongoengine==0.15.0
 
+Markdown==3.1.1
+Bleach==3.1.0
 
 # Possibly dead
 imagesize==0.7.1
 Jinja2==2.8
-Markdown==2.6.7
-MarkupSafe==0.23
 pep8==1.7.0
 six==1.10.0
 snowballstemmer==1.2.1
diff --git a/scipost/static/scipost/assets/css/_general.scss b/scipost/static/scipost/assets/css/_general.scss
index a76c5a9e1c6643835ec759a26f423b591d111f70..5de4f5179fc141cadeabe788412fe537b873bbf4 100644
--- a/scipost/static/scipost/assets/css/_general.scss
+++ b/scipost/static/scipost/assets/css/_general.scss
@@ -13,6 +13,12 @@
     background-color: transparent;
 }
 
+blockquote {
+    margin-left: 0.5rem;
+    padding-left: 0.5rem;
+    border-left: 2px solid grey;
+}
+
 body #MathJax_Message {
     left: 1rem;
     bottom: 1rem;
diff --git a/scipost/static/scipost/ticket-preview.js b/scipost/static/scipost/ticket-preview.js
index f1973bb83fd48e6f4eab4aa158fa68bc7dcf342e..1cbef9045e0be37c62a59aa0a2a3d95527536995 100644
--- a/scipost/static/scipost/ticket-preview.js
+++ b/scipost/static/scipost/ticket-preview.js
@@ -18,7 +18,7 @@ $('#runPreviewButton').on('click', function(){
     $('#preview-title').text($('#id_title').val());
     $.ajax({
     	type: "POST",
-    	url: "/process_markup/",
+    	url: "/markup/process/",
     	data: {
     	    csrfmiddlewaretoken: $('input[name=csrfmiddlewaretoken]').val(),
 	    markup_text: $('#id_description').val(),
@@ -33,7 +33,7 @@ $('#runPreviewButton').on('click', function(){
 		$('#runPreviewButton').show();
 		alert("An error has occurred while processing the text:\n\n" + data.errors);
 	    }
-    	    $('#preview-description').html(data.processed_markup);
+    	    $('#preview-description').html(data.processed);
 	    let preview = document.getElementById('preview-description');
     	    MathJax.Hub.Queue(["Typeset",MathJax.Hub, preview]);
     	},
@@ -43,6 +43,6 @@ $('#runPreviewButton').on('click', function(){
     });
     $('#runPreviewButton').hide();
     $('#preview-title').css('background', '#f1f1f1');
-    $('#preview-description').css('background', '#f8f8f8');
+    $('#preview-description').css('background', '#ffffff');
     $('#submitButton').show();
 }).trigger('change');
diff --git a/scipost/urls.py b/scipost/urls.py
index c66ded46265815a2b3e5b4ad5d8d0083f5cc2a0b..047ca858c107b8d12efe2197455fd22deed81697 100644
--- a/scipost/urls.py
+++ b/scipost/urls.py
@@ -23,12 +23,6 @@ urlpatterns = [
     # Utilities:
     # Search
     url(r'^search', views.SearchView.as_view(), name='search'),
-    # preprocess reStructuredText
-    url(
-        r'^process_markup/$',
-        views.process_markup,
-        name='process_markup'
-    ),
 
     url(r'^$', views.index, name='index'),
     url(r'^files/secure/(?P<path>.*)$', views.protected_serve, name='secure_file'),
diff --git a/scipost/views.py b/scipost/views.py
index 6064a4ab6f9af1bb4a3ed1b693e8231a93f4b756..244c8c0cb5e9c2558745640e171146896c6010bf 100644
--- a/scipost/views.py
+++ b/scipost/views.py
@@ -55,7 +55,6 @@ from .utils import EMAIL_FOOTER, SCIPOST_SUMMARY_FOOTER, SCIPOST_SUMMARY_FOOTER_
 from colleges.permissions import fellowship_or_admin_required
 from commentaries.models import Commentary
 from comments.models import Comment
-from common.forms import MarkupTextForm
 from invitations.constants import STATUS_REGISTERED
 from invitations.models import RegistrationInvitation
 from journals.models import Journal, Publication, PublicationAuthorsTable
@@ -116,13 +115,6 @@ class SearchView(SearchView):
         return ctx
 
 
-def process_markup(request):
-    form = MarkupTextForm(request.POST or None)
-    if form.is_valid():
-        return JsonResponse(form.get_processed_markup())
-    return JsonResponse({})
-
-
 #############
 # Main view
 #############