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