Switch markdown editor to Martor (#1159)

This commit is contained in:
Guanzhong Chen 2020-03-31 17:10:22 -04:00 committed by GitHub
parent 12860ede66
commit 196311ab17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 370 additions and 233 deletions

1
.gitignore vendored
View File

@ -10,4 +10,5 @@ resources/style.css
resources/content-description.css
resources/ranks.css
resources/table.css
resources/martor-description.css
sass_processed

View File

@ -237,6 +237,7 @@ INSTALLED_APPS += (
'statici18n',
'impersonate',
'django_jinja',
'martor',
)
MIDDLEWARE = (
@ -386,6 +387,7 @@ MARKDOWN_USER_LARGE_STYLE = {
}
MARKDOWN_STYLES = {
'default': MARKDOWN_DEFAULT_STYLE,
'comment': MARKDOWN_DEFAULT_STYLE,
'self-description': MARKDOWN_USER_LARGE_STYLE,
'problem': MARKDOWN_ADMIN_EDITABLE_STYLE,
@ -400,6 +402,22 @@ MARKDOWN_STYLES = {
'ticket': MARKDOWN_USER_LARGE_STYLE,
}
MARTOR_ENABLE_CONFIGS = {
'imgur': 'true',
'mention': 'false',
'jquery': 'true',
'living': 'false',
'spellcheck': 'false',
'hljs': 'false',
}
MARTOR_MARKDOWNIFY_URL = '/widgets/preview/default'
MARTOR_SEARCH_USERS_URL = '/widgets/martor/search-user'
MARTOR_UPLOAD_URL = '/widgets/martor/upload-image'
# Directory under MEDIA_ROOT to use to store image uploaded through martor.
MARTOR_UPLOAD_MEDIA_DIR = 'martor'
MARTOR_UPLOAD_SAFE_EXTS = {'.jpg', '.png', '.gif'}
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

View File

@ -5,10 +5,11 @@ from django.contrib.auth import views as auth_views
from django.contrib.sitemaps.views import sitemap
from django.http import Http404, HttpResponsePermanentRedirect
from django.templatetags.static import static
from django.urls import reverse
from django.urls import path, reverse
from django.utils.functional import lazystr
from django.utils.translation import ugettext_lazy as _
from django.views.generic import RedirectView
from martor.views import markdown_search_user
from judge.feed import AtomBlogFeed, AtomCommentFeed, AtomProblemFeed, BlogFeed, CommentFeed, ProblemFeed
from judge.forms import CustomAuthenticationForm
@ -23,6 +24,7 @@ from judge.views.register import ActivationView, RegistrationView
from judge.views.select2 import AssigneeSelect2View, CommentSelect2View, ContestSelect2View, \
ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \
UserSearchSelect2View, UserSelect2View
from judge.views.widgets import martor_image_uploader
admin.autodiscover()
@ -292,6 +294,7 @@ urlpatterns = [
])),
url(r'^preview/', include([
url(r'^default$', preview.DefaultMarkdownPreviewView.as_view(), name='default_preview'),
url(r'^problem$', preview.ProblemMarkdownPreviewView.as_view(), name='problem_preview'),
url(r'^blog$', preview.BlogMarkdownPreviewView.as_view(), name='blog_preview'),
url(r'^contest$', preview.ContestMarkdownPreviewView.as_view(), name='contest_preview'),
@ -302,6 +305,11 @@ urlpatterns = [
url(r'^license$', preview.LicenseMarkdownPreviewView.as_view(), name='license_preview'),
url(r'^ticket$', preview.TicketMarkdownPreviewView.as_view(), name='ticket_preview'),
])),
path('martor/', include([
path('upload-image', martor_image_uploader, name='martor_image_uploader'),
path('search-user', markdown_search_user, name='martor_search_user'),
])),
])),
url(r'^feed/', include([

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin
from judge.models import Comment
from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
from judge.widgets import AdminHeavySelect2Widget, AdminMartorWidget
class CommentForm(ModelForm):
@ -13,9 +13,8 @@ class CommentForm(ModelForm):
widgets = {
'author': AdminHeavySelect2Widget(data_view='profile_select2'),
'parent': AdminHeavySelect2Widget(data_view='comment_select2'),
'body': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('comment_preview')}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview'))
class CommentAdmin(VersionAdmin):

View File

@ -15,8 +15,8 @@ from django_ace import AceWidget
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
from judge.ratings import rate_contest
from judge.utils.views import NoBatchDeleteMixin
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminMartorWidget, \
AdminSelect2MultipleWidget, AdminSelect2Widget
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
@ -39,11 +39,9 @@ class ContestTagAdmin(admin.ModelAdmin):
actions_on_top = True
actions_on_bottom = True
form = ContestTagForm
if AdminPagedownWidget is not None:
formfield_overrides = {
TextField: {'widget': AdminPagedownWidget},
}
formfield_overrides = {
TextField: {'widget': AdminMartorWidget},
}
def save_model(self, request, obj, form, change):
super(ContestTagAdmin, self).save_model(request, obj, form, change)
@ -105,11 +103,9 @@ class ContestForm(ModelForm):
attrs={'style': 'width: 100%'}),
'view_contest_scoreboard': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
attrs={'style': 'width: 100%'}),
'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('contest_preview')}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview'))
class ContestAdmin(NoBatchDeleteMixin, VersionAdmin):
fieldsets = (

View File

@ -9,7 +9,7 @@ from reversion.admin import VersionAdmin
from judge.dblock import LockModel
from judge.models import NavigationBar
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminMartorWidget
class NavigationBarAdmin(DraggableMPTTAdmin):
@ -49,12 +49,10 @@ class BlogPostForm(ModelForm):
class Meta:
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'content': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('blog_preview')}),
'summary': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('blog_preview')}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
class BlogPostAdmin(VersionAdmin):
fieldsets = (
@ -84,16 +82,13 @@ class SolutionForm(ModelForm):
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}),
'content': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('solution_preview')}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
class LicenseForm(ModelForm):
class Meta:
if HeavyPreviewAdminPageDownWidget is not None:
widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))}
widgets = {'text': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('license_preview')})}
class LicenseAdmin(admin.ModelAdmin):

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext, gettext_lazy as _
from reversion.admin import VersionAdmin
from judge.models import Organization
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminMartorWidget
class OrganizationForm(ModelForm):
@ -14,9 +14,8 @@ class OrganizationForm(ModelForm):
widgets = {
'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'registrant': AdminHeavySelect2Widget(data_view='profile_select2'),
'about': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('organization_preview')}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview'))
class OrganizationAdmin(VersionAdmin):

View File

@ -12,8 +12,8 @@ from reversion.admin import VersionAdmin
from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution
from judge.utils.views import NoBatchDeleteMixin
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \
CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminMartorWidget, AdminSelect2MultipleWidget, \
AdminSelect2Widget, CheckboxSelectMultipleWithSelectAll
class ProblemForm(ModelForm):
@ -40,9 +40,8 @@ class ProblemForm(ModelForm):
attrs={'style': 'width: 100%'}),
'types': AdminSelect2MultipleWidget,
'group': AdminSelect2Widget,
'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('problem_preview')}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))
class ProblemCreatorListFilter(admin.SimpleListFilter):
@ -71,8 +70,7 @@ class LanguageLimitInline(admin.TabularInline):
class ProblemClarificationForm(ModelForm):
class Meta:
if HeavyPreviewPageDownWidget is not None:
widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))}
widgets = {'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('comment_preview')})}
class ProblemClarificationInline(admin.StackedInline):
@ -90,11 +88,9 @@ class ProblemSolutionForm(ModelForm):
class Meta:
widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'content': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('solution_preview')}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
class ProblemSolutionInline(admin.StackedInline):
model = Solution
@ -105,8 +101,7 @@ class ProblemSolutionInline(admin.StackedInline):
class ProblemTranslationForm(ModelForm):
class Meta:
if HeavyPreviewAdminPageDownWidget is not None:
widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))}
widgets = {'description': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('problem_preview')})}
class ProblemTranslationInline(admin.StackedInline):

View File

@ -1,5 +1,6 @@
from django.contrib import admin
from django.forms import ModelForm
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin
@ -7,7 +8,7 @@ from reversion.admin import VersionAdmin
from django_ace import AceWidget
from judge.models import Profile
from judge.utils.views import NoBatchDeleteMixin
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
from judge.widgets import AdminMartorWidget, AdminSelect2Widget
class ProfileForm(ModelForm):
@ -26,9 +27,8 @@ class ProfileForm(ModelForm):
'language': AdminSelect2Widget,
'ace_theme': AdminSelect2Widget,
'current_contest': AdminSelect2Widget,
'about': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('profile_preview')}),
}
if AdminPagedownWidget is not None:
widgets['about'] = AdminPagedownWidget
class TimezoneFilter(admin.SimpleListFilter):

View File

@ -11,7 +11,7 @@ from reversion.admin import VersionAdmin
from django_ace import AceWidget
from judge.models import Judge, Problem
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminMartorWidget
class LanguageForm(ModelForm):
@ -23,8 +23,7 @@ class LanguageForm(ModelForm):
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
class Meta:
if AdminPagedownWidget is not None:
widgets = {'description': AdminPagedownWidget}
widgets = {'description': AdminMartorWidget}
class LanguageAdmin(VersionAdmin):
@ -70,9 +69,7 @@ django.jQuery(document).ready(function ($) {{
class JudgeAdminForm(ModelForm):
class Meta:
widgets = {'auth_key': GenerateKeyTextInput}
if AdminPagedownWidget is not None:
widgets['description'] = AdminPagedownWidget
widgets = {'auth_key': GenerateKeyTextInput, 'description': AdminMartorWidget}
class JudgeAdmin(VersionAdmin):
@ -86,6 +83,9 @@ class JudgeAdmin(VersionAdmin):
)
list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip')
ordering = ['-online', 'name']
formfield_overrides = {
TextField: {'widget': AdminMartorWidget},
}
def get_urls(self):
return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'),
@ -113,8 +113,3 @@ class JudgeAdmin(VersionAdmin):
if result and obj is not None:
return not obj.online
return result
if AdminPagedownWidget is not None:
formfield_overrides = {
TextField: {'widget': AdminPagedownWidget},
}

View File

@ -4,16 +4,15 @@ from django.forms import ModelForm
from django.urls import reverse_lazy
from judge.models import TicketMessage
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminMartorWidget
class TicketMessageForm(ModelForm):
class Meta:
widgets = {
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'body': AdminMartorWidget(attrs={'data-markdownfy-url': reverse_lazy('ticket_preview')}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview'))
class TicketMessageInline(StackedInline):

View File

@ -5,7 +5,7 @@ from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
class MarkdownPreviewView(TemplateResponseMixin, ContextMixin, View):
def post(self, request, *args, **kwargs):
try:
self.preview_data = data = request.POST['preview']
self.preview_data = data = request.POST['content']
except KeyError:
return HttpResponseBadRequest('No preview data specified.')
@ -48,3 +48,7 @@ class LicenseMarkdownPreviewView(MarkdownPreviewView):
class TicketMarkdownPreviewView(MarkdownPreviewView):
template_name = 'ticket/preview.html'
class DefaultMarkdownPreviewView(MarkdownPreviewView):
template_name = 'default-preview.html'

View File

@ -1,11 +1,18 @@
import json
import os
import uuid
from urllib.parse import urljoin
import requests
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
from django.views.generic import View
from martor.api import imgur_uploader
from judge.models import Submission
@ -67,3 +74,29 @@ class DetectTimezone(View):
'askgeo': self.askgeo,
'geonames': self.geonames,
}.get(backend, self.default)(lat, long)
def django_uploader(image):
ext = os.path.splitext(image.name)[1]
if ext not in settings.MARTOR_UPLOAD_SAFE_EXTS:
ext = '.png'
name = str(uuid.uuid4()) + ext
default_storage.save(os.path.join(settings.MARTOR_UPLOAD_MEDIA_DIR, name), image)
url_base = getattr(settings, 'MARTOR_UPLOAD_URL_PREFIX',
urljoin(settings.MEDIA_URL, settings.MARTOR_UPLOAD_MEDIA_DIR))
if not url_base.endswith('/'):
url_base += '/'
return json.dumps({'status': 200, 'name': '', 'link': urljoin(url_base, name)})
@login_required
def martor_image_uploader(request):
if request.method != 'POST' or not request.is_ajax() or 'markdown-image-upload' not in request.FILES:
return HttpResponseBadRequest('Invalid request')
image = request.FILES['markdown-image-upload']
if request.user.is_staff:
data = django_uploader(image)
else:
data = imgur_uploader(image)
return HttpResponse(data, content_type='application/json')

View File

@ -1,4 +1,5 @@
from judge.widgets.checkbox import CheckboxSelectMultipleWithSelectAll
from judge.widgets.martor import *
from judge.widgets.mixins import CompressorWidgetMixin
from judge.widgets.pagedown import *
from judge.widgets.select2 import *

17
judge/widgets/martor.py Normal file
View File

@ -0,0 +1,17 @@
from martor.widgets import AdminMartorWidget as OldAdminMartorWidget, MartorWidget as OldMartorWidget
__all__ = ['MartorWidget', 'AdminMartorWidget']
class MartorWidget(OldMartorWidget):
class Media:
css = {
'all': ['martor-description.css'],
}
js = ['martor-mathjax.js']
class AdminMartorWidget(OldAdminMartorWidget):
class Media:
css = MartorWidget.Media.css
js = ['admin/js/jquery.init.js', 'martor-mathjax.js']

View File

@ -14,7 +14,8 @@ if ! [ -x "$(command -v autoprefixer)" ]; then
exit 1
fi
FILES=(sass_processed/style.css sass_processed/content-description.css sass_processed/table.css sass_processed/ranks.css)
FILES=(sass_processed/style.css sass_processed/content-description.css sass_processed/table.css
sass_processed/ranks.css sass_processed/martor-description.css)
cd `dirname $0`
sass --update resources:sass_processed

View File

@ -31,3 +31,4 @@ celery
-e git://github.com/DMOJ/ansi2html.git#egg=ansi2html
sqlparse
lupa
-e git://github.com/DMOJ/martor.git#egg=martor

View File

@ -0,0 +1,178 @@
@import "vars";
@mixin content-description {
line-height: 1.5em;
font-size: 1em;
font-family: "Segoe UI", "Lucida Grande", Arial, sans-serif;
p {
margin: 1em 0 !important;
padding: 0 !important;
}
img {
max-width: 100%;
height: auto;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
color: #111;
margin-bottom: 0.75em;
padding: 0;
background: 0;
}
h3, h4, h5, h6 {
font-weight: bold;
}
h1 {
font-size: 2.5em;
}
h2 {
font-size: 2em;
}
h3 {
font-size: 1.6em;
margin: 0;
padding: 0;
}
h4 {
font-size: 1.4em;
border-bottom: 1px solid #EEE;
line-height: 1.225;
padding-bottom: 0.3em;
padding-top: 0.5em;
}
h5 {
font-size: 1.15em;
margin-top: 0;
}
h6 {
font-size: 0.9em;
}
blockquote {
color: #666;
margin: 0;
padding-left: 1.5em;
border-left: 0.5em #EEE solid;
}
hr {
display: block;
height: 0;
border: 0;
font-style: italic;
border-bottom: 1px solid $border_gray;
margin: 25px 0 20px 0;
padding: 0;
}
pre, code, kbd, samp, span.code {
color: #000;
page-break-inside: avoid;
font-family: $monospace-fonts;
font-size: 0.98em;
}
code, span.code {
font-family: $monospace-fonts !important;
margin: 0 2px;
padding: 0 5px;
border: 1px solid $border_gray;
background-color: #f8f8f8;
border-radius: $widget_border_radius;
font-size: 0.95em;
color: #444;
}
pre {
code, div.code {
border: 0;
line-height: 1em;
margin: 0;
padding: 0;
background: transparent;
font-size: 1em;
color: black;
}
white-space: pre-wrap;
word-wrap: break-word;
margin: 1.5em 0 1.5em 0;
padding: 1em;
border: 1px solid $border_gray;
background-color: #f8f8f8;
color: black;
border-radius: $widget_border_radius;
}
b, strong {
font-weight: bold;
}
dfn {
font-style: italic;
}
ins {
background: #ff9;
color: #000;
text-decoration: none;
}
mark {
background: #ff0;
color: #000;
font-style: italic;
font-weight: bold;
}
sub {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
top: -0.5em;
}
sub {
bottom: -0.25em;
}
ul, ol {
padding: 0 0 0 2em !important;
}
li p:last-child {
margin: 0;
}
dd {
margin: 0 0 0 2em;
}
img {
border: 0;
-ms-interpolation-mode: bicubic;
vertical-align: middle;
}
td {
vertical-align: top;
}
}

View File

@ -1,180 +1,7 @@
@import "vars";
@import "base-description";
.content-description {
line-height: 1.5em;
font-size: 1em;
font-family: "Segoe UI", "Lucida Grande", Arial, sans-serif;
p {
margin: 1em 0 !important;
padding: 0 !important;
}
img {
max-width: 100%;
height: auto;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
color: #111;
margin-bottom: 0.75em;
padding: 0;
background: 0;
}
h3, h4, h5, h6 {
font-weight: bold;
}
h1 {
font-size: 2.5em;
}
h2 {
font-size: 2em;
}
h3 {
font-size: 1.6em;
margin: 0;
padding: 0;
}
h4 {
font-size: 1.4em;
border-bottom: 1px solid #EEE;
line-height: 1.225;
padding-bottom: 0.3em;
padding-top: 0.5em;
}
h5 {
font-size: 1.15em;
margin-top: 0;
}
h6 {
font-size: 0.9em;
}
blockquote {
color: #666;
margin: 0;
padding-left: 1.5em;
border-left: 0.5em #EEE solid;
}
hr {
display: block;
height: 0;
border: 0;
font-style: italic;
border-bottom: 1px solid $border_gray;
margin: 25px 0 20px 0;
padding: 0;
}
pre, code, kbd, samp, span.code {
color: #000;
page-break-inside: avoid;
font-family: $monospace-fonts;
font-size: 0.98em;
}
code, span.code {
font-family: $monospace-fonts !important;
margin: 0 2px;
padding: 0 5px;
border: 1px solid $border_gray;
background-color: #f8f8f8;
border-radius: $widget_border_radius;
font-size: 0.95em;
color: #444;
}
pre {
code, div.code {
border: 0;
line-height: 1em;
margin: 0;
padding: 0;
background: transparent;
font-size: 1em;
color: black;
}
white-space: pre-wrap;
word-wrap: break-word;
margin: 1.5em 0 1.5em 0;
padding: 1em;
border: 1px solid $border_gray;
background-color: #f8f8f8;
color: black;
border-radius: $widget_border_radius;
}
b, strong {
font-weight: bold;
}
dfn {
font-style: italic;
}
ins {
background: #ff9;
color: #000;
text-decoration: none;
}
mark {
background: #ff0;
color: #000;
font-style: italic;
font-weight: bold;
}
sub {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
top: -0.5em;
}
sub {
bottom: -0.25em;
}
ul, ol {
padding: 0 0 0 2em !important;
}
li p:last-child {
margin: 0;
}
dd {
margin: 0 0 0 2em;
}
img {
border: 0;
-ms-interpolation-mode: bicubic;
vertical-align: middle;
}
td {
vertical-align: top;
}
@include content-description;
}
.display-math {

View File

@ -19,7 +19,7 @@ $(function () {
if (text) {
$preview.addClass('dmmd-preview-stale');
$.post(preview_url, {
preview: text,
content: text,
csrfmiddlewaretoken: $.cookie('csrftoken')
}, function (result) {
$content.html(result);

View File

@ -0,0 +1,35 @@
@import "base-description";
form .martor-preview {
@include content-description;
.martor-preview ul li {
list-style: unset !important;
}
.martor-preview ul, .martor-preview ol {
margin-left: 0 !important;
}
}
.main-martor-fullscreen {
z-index: 99999 !important;
}
.section-martor {
div[data-tab="editor-tab-description"] {
padding: 0;
}
.markdown-selector.markdown-emoji {
display: none !important;
}
.tab-martor-menu a.item {
font-size: 0.8rem;
}
.martor-field {
height: 400px;
}
}

View File

@ -0,0 +1,34 @@
jQuery(function ($) {
$(document).on('martor:preview', function (e, $content) {
function update_math() {
MathJax.Hub.Queue(['Typeset', MathJax.Hub, $content[0]], function () {
$content.find('.tex-image').hide();
$content.find('.tex-text').show();
});
}
var $jax = $content.find('.require-mathjax-support');
if ($jax.length) {
if (!('MathJax' in window)) {
$.ajax({
type: 'GET',
url: $jax.attr('data-config'),
dataType: 'script',
cache: true,
success: function () {
window.MathJax.loader = {typeset: false};
$.ajax({
type: 'GET',
url: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML',
dataType: 'script',
cache: true,
success: update_math
});
}
});
} else {
update_math();
}
}
})
});

View File

@ -0,0 +1 @@
{{ preview_data|markdown('default') }}