diff --git a/dmoj/settings.py b/dmoj/settings.py index 00db9a1..9b1b268 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -130,7 +130,6 @@ USE_SELENIUM = False SELENIUM_CUSTOM_CHROME_PATH = None SELENIUM_CHROMEDRIVER_PATH = "chromedriver" -PYGMENT_THEME = "pygment-github.css" INLINE_JQUERY = True INLINE_FONTAWESOME = True JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" diff --git a/judge/highlight_code.py b/judge/highlight_code.py index ce407de..f5c91e3 100644 --- a/judge/highlight_code.py +++ b/judge/highlight_code.py @@ -1,44 +1,13 @@ from django.utils.html import escape, mark_safe +from judge.markdown import markdown __all__ = ["highlight_code"] -def _make_pre_code(code): - return mark_safe("
" + escape(code) + "
") +def highlight_code(code, language, linenos=True, title=None): + linenos_option = 'linenums="1"' if linenos else "" + title_option = f'title="{title}"' if title else "" + options = f"{{.{language} {linenos_option} {title_option}}}" - -try: - import pygments - import pygments.lexers - import pygments.formatters - import pygments.util -except ImportError: - - def highlight_code(code, language, cssclass=None): - return _make_pre_code(code) - -else: - - def highlight_code(code, language, cssclass="codehilite", linenos=True): - try: - lexer = pygments.lexers.get_lexer_by_name(language) - except pygments.util.ClassNotFound: - return _make_pre_code(code) - - if linenos: - return mark_safe( - pygments.highlight( - code, - lexer, - pygments.formatters.HtmlFormatter( - cssclass=cssclass, linenos="table", wrapcode=True - ), - ) - ) - return mark_safe( - pygments.highlight( - code, - lexer, - pygments.formatters.HtmlFormatter(cssclass=cssclass, wrapcode=True), - ) - ) + value = f"```{options}\n{code}\n```\n" + return mark_safe(markdown(value)) diff --git a/judge/jinja2/markdown/__init__.py b/judge/jinja2/markdown/__init__.py index fa78791..18355d4 100644 --- a/judge/jinja2/markdown/__init__.py +++ b/judge/jinja2/markdown/__init__.py @@ -1,112 +1,7 @@ from .. import registry -import markdown as _markdown -import bleach -from django.utils.html import escape -from bs4 import BeautifulSoup -from pymdownx import superfences - - -EXTENSIONS = [ - "pymdownx.arithmatex", - "pymdownx.magiclink", - "pymdownx.betterem", - "pymdownx.details", - "pymdownx.emoji", - "pymdownx.inlinehilite", - "pymdownx.superfences", - "pymdownx.tasklist", - "markdown.extensions.footnotes", - "markdown.extensions.attr_list", - "markdown.extensions.def_list", - "markdown.extensions.tables", - "markdown.extensions.admonition", - "nl2br", - "mdx_breakless_lists", -] - -EXTENSION_CONFIGS = { - "pymdownx.superfences": { - "custom_fences": [ - { - "name": "sample", - "class": "no-border", - "format": superfences.fence_code_format, - } - ] - }, -} - -ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [ - "img", - "center", - "iframe", - "div", - "span", - "table", - "tr", - "td", - "th", - "tr", - "pre", - "code", - "p", - "hr", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "thead", - "tbody", - "sup", - "dl", - "dt", - "dd", - "br", - "details", - "summary", -] - -ALLOWED_ATTRS = ["src", "width", "height", "href", "class", "open"] +from judge.markdown import markdown as _markdown @registry.filter def markdown(value, lazy_load=False): - extensions = EXTENSIONS - html = _markdown.markdown( - value, extensions=extensions, extension_configs=EXTENSION_CONFIGS - ) - - # Don't clean mathjax - hash_script_tag = {} - soup = BeautifulSoup(html, "html.parser") - for script_tag in soup.find_all("script"): - allow_math_types = ["math/tex", "math/tex; mode=display"] - if script_tag.attrs.get("type", False) in allow_math_types: - hash_script_tag[str(hash(str(script_tag)))] = str(script_tag) - - for hashed_tag in hash_script_tag: - tag = hash_script_tag[hashed_tag] - html = html.replace(tag, hashed_tag) - - html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS) - - for hashed_tag in hash_script_tag: - tag = hash_script_tag[hashed_tag] - html = html.replace(hashed_tag, tag) - - if not html: - html = escape(value) - if lazy_load: - soup = BeautifulSoup(html, features="html.parser") - for img in soup.findAll("img"): - if img.get("src"): - img["data-src"] = img["src"] - img["src"] = "" - for img in soup.findAll("iframe"): - if img.get("src"): - img["data-src"] = img["src"] - img["src"] = "" - html = str(soup) - return '
%s
' % html + return _markdown(value, lazy_load) diff --git a/judge/management/commands/render_pdf.py b/judge/management/commands/render_pdf.py index 238000f..7dc9e33 100644 --- a/judge/management/commands/render_pdf.py +++ b/judge/management/commands/render_pdf.py @@ -96,7 +96,7 @@ class Command(BaseCommand): .replace("'//", "'https://") ) maker.title = problem_name - for file in ("style.css", "pygment-github.css", "mathjax3_config.js"): + for file in ("style.css", "mathjax3_config.js"): maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.make(debug=True) if not maker.success: diff --git a/judge/markdown.py b/judge/markdown.py new file mode 100644 index 0000000..4f37cfb --- /dev/null +++ b/judge/markdown.py @@ -0,0 +1,117 @@ +import markdown as _markdown +import bleach +from django.utils.html import escape +from bs4 import BeautifulSoup +from pymdownx import superfences + + +EXTENSIONS = [ + "pymdownx.arithmatex", + "pymdownx.magiclink", + "pymdownx.betterem", + "pymdownx.details", + "pymdownx.emoji", + "pymdownx.inlinehilite", + "pymdownx.superfences", + "pymdownx.highlight", + "pymdownx.tasklist", + "markdown.extensions.footnotes", + "markdown.extensions.attr_list", + "markdown.extensions.def_list", + "markdown.extensions.tables", + "markdown.extensions.admonition", + "nl2br", + "mdx_breakless_lists", +] + +EXTENSION_CONFIGS = { + "pymdownx.superfences": { + "custom_fences": [ + { + "name": "sample", + "class": "no-border", + "format": superfences.fence_code_format, + } + ], + }, + "pymdownx.highlight": { + "auto_title": True, + "auto_title_map": { + "Text Only": "", + }, + }, +} + +ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [ + "img", + "center", + "iframe", + "div", + "span", + "table", + "tr", + "td", + "th", + "tr", + "pre", + "code", + "p", + "hr", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "thead", + "tbody", + "sup", + "dl", + "dt", + "dd", + "br", + "details", + "summary", +] + +ALLOWED_ATTRS = ["src", "width", "height", "href", "class", "open"] + + +def markdown(value, lazy_load=False): + extensions = EXTENSIONS + html = _markdown.markdown( + value, extensions=extensions, extension_configs=EXTENSION_CONFIGS + ) + + # Don't clean mathjax + hash_script_tag = {} + soup = BeautifulSoup(html, "html.parser") + for script_tag in soup.find_all("script"): + allow_math_types = ["math/tex", "math/tex; mode=display"] + if script_tag.attrs.get("type", False) in allow_math_types: + hash_script_tag[str(hash(str(script_tag)))] = str(script_tag) + + for hashed_tag in hash_script_tag: + tag = hash_script_tag[hashed_tag] + html = html.replace(tag, hashed_tag) + + html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS) + + for hashed_tag in hash_script_tag: + tag = hash_script_tag[hashed_tag] + html = html.replace(hashed_tag, tag) + + if not html: + html = escape(value) + if lazy_load: + soup = BeautifulSoup(html, features="html.parser") + for img in soup.findAll("img"): + if img.get("src"): + img["data-src"] = img["src"] + img["src"] = "" + for img in soup.findAll("iframe"): + if img.get("src"): + img["data-src"] = img["src"] + img["src"] = "" + html = str(soup) + return '
%s
' % html diff --git a/judge/template_context.py b/judge/template_context.py index f72da6a..ced05ae 100644 --- a/judge/template_context.py +++ b/judge/template_context.py @@ -24,7 +24,6 @@ def get_resource(request): scheme = "http" return { - "PYGMENT_THEME": settings.PYGMENT_THEME, "INLINE_JQUERY": settings.INLINE_JQUERY, "INLINE_FONTAWESOME": settings.INLINE_FONTAWESOME, "JQUERY_JS": settings.JQUERY_JS, diff --git a/judge/views/problem.py b/judge/views/problem.py index 41000a4..7ba04ab 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -406,7 +406,7 @@ class ProblemPdfView(ProblemMixin, SingleObjectMixin, View): .replace("'//", "'https://") ) maker.title = problem_name - assets = ["style.css", "pygment-github.css"] + assets = ["style.css"] if maker.math_engine == "jax": assets.append("mathjax3_config.js") for file in assets: diff --git a/judge/views/problem_data.py b/judge/views/problem_data.py index 0525395..47ad4e3 100644 --- a/judge/views/problem_data.py +++ b/judge/views/problem_data.py @@ -344,7 +344,7 @@ def problem_init_view(request, problem): "problem/yaml.html", { "raw_source": data, - "highlighted_source": highlight_code(data, "yaml", linenos=False), + "highlighted_source": highlight_code(data, "yaml", linenos=True), "title": _("Generated init.yml for %s") % problem.name, "content_title": mark_safe( escape(_("Generated init.yml for %s")) diff --git a/judge/views/submission.py b/judge/views/submission.py index 7158c01..8baadac 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -113,22 +113,6 @@ class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, Deta ) -class SubmissionSource(SubmissionDetailBase): - template_name = "submission/source.html" - - def get_queryset(self): - return super().get_queryset().select_related("source") - - def get_context_data(self, **kwargs): - context = super(SubmissionSource, self).get_context_data(**kwargs) - submission = self.object - context["raw_source"] = submission.source.source.rstrip("\n") - context["highlighted_source"] = highlight_code( - submission.source.source, submission.language.pygments, linenos=False - ) - return context - - def get_hidden_subtasks(request, submission): contest = submission.contest_object if contest and contest.is_editable_by(request.user): @@ -227,9 +211,11 @@ class SubmissionStatus(SubmissionDetailBase): ) context["time_limit"] = submission.problem.time_limit context["can_see_testcases"] = False - context["raw_source"] = submission.source.source.rstrip("\n") context["highlighted_source"] = highlight_code( - submission.source.source, submission.language.pygments, linenos=False + submission.source.source, + submission.language.pygments, + linenos=True, + title=submission.language, ) contest = submission.contest_or_none @@ -265,7 +251,7 @@ class SubmissionTestCaseQuery(SubmissionStatus): return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs) -class SubmissionSourceRaw(SubmissionSource): +class SubmissionSourceRaw(SubmissionDetailBase): def get(self, request, *args, **kwargs): submission = self.get_object() return HttpResponse(submission.source.source, content_type="text/plain") diff --git a/judge/widgets/pagedown.py b/judge/widgets/pagedown.py index c30940b..e0f812c 100644 --- a/judge/widgets/pagedown.py +++ b/judge/widgets/pagedown.py @@ -117,7 +117,6 @@ else: class Media: css = { "all": [ - "pygment-github.css", "table.css", "ranks.css", "dmmd-preview.css", diff --git a/resources/common.js b/resources/common.js index cae4e96..38db21b 100644 --- a/resources/common.js +++ b/resources/common.js @@ -277,6 +277,34 @@ function registerPopper($trigger, $dropdown) { }) } +function populateCopyButton() { + var copyButton; + $('pre code').each(function () { + $(this).before($('
', {'class': 'copy-clipboard'}) + .append(copyButton = $('', { + 'class': 'btn-clipboard', + 'data-clipboard-text': $(this).text(), + 'title': 'Click to copy' + }).append('Copy'))); + + $(copyButton.get(0)).mouseleave(function () { + $(this).attr('class', 'btn-clipboard'); + $(this).removeAttr('aria-label'); + }); + + var curClipboard = new Clipboard(copyButton.get(0)); + + curClipboard.on('success', function (e) { + e.clearSelection(); + showTooltip(e.trigger, 'Copied!'); + }); + + curClipboard.on('error', function (e) { + showTooltip(e.trigger, fallbackMessage(e.action)); + }); + }); +} + function onWindowReady() { // http://stackoverflow.com/a/1060034/1090657 var hidden = 'hidden'; @@ -370,31 +398,8 @@ function onWindowReady() { }) $('#logout').on('click', () => $('#logout-form').submit()); - var copyButton; - $('pre code').each(function () { - $(this).parent().before($('
', {'class': 'copy-clipboard'}) - .append(copyButton = $('', { - 'class': 'btn-clipboard', - 'data-clipboard-text': $(this).text(), - 'title': 'Click to copy' - }).text('Copy'))); - - $(copyButton.get(0)).mouseleave(function () { - $(this).attr('class', 'btn-clipboard'); - $(this).removeAttr('aria-label'); - }); - - var curClipboard = new Clipboard(copyButton.get(0)); - - curClipboard.on('success', function (e) { - e.clearSelection(); - showTooltip(e.trigger, 'Copied!'); - }); - - curClipboard.on('error', function (e) { - showTooltip(e.trigger, fallbackMessage(e.action)); - }); - }); + populateCopyButton(); + $('a').click(function() { var href = $(this).attr('href'); if (!href || href === '#' || href.startsWith("javascript")) { diff --git a/resources/content-description.scss b/resources/content-description.scss index 442be32..b13cd35 100644 --- a/resources/content-description.scss +++ b/resources/content-description.scss @@ -21,11 +21,9 @@ font-family: $monospace-fonts !important; margin: 0 2px; padding: 0 5px; - border: 1px solid $border_gray; - background-color: #f8f8f8; + background-color: var(--md-code-bg-color); border-radius: $widget_border_radius; - font-size: 0.95em; - color: #444; + color: var(--md-code-fg-color); } pre { @@ -36,17 +34,16 @@ padding: 0; background: transparent; font-size: 1em; - color: black; + color: var(--md-code-fg-color); } white-space: pre-wrap; word-wrap: break-word; - padding: 1em; - border: 1px solid $border_gray; - background-color: #f8f8f8; - color: black; - border-radius: $widget_border_radius; + padding: 0.5em 1em; + background-color: var(--md-code-bg-color); + color: var(--md-code-fg-color); + border-radius: 3px; } pre.no-border { @@ -57,6 +54,10 @@ border-radius: none; } + .linenos pre { + padding-right: 0; + } + b, strong { font-weight: bold; } @@ -170,7 +171,10 @@ pre { } -@media (min-width: 700px) { +@media (min-width: 800px) { + .content-description pre:has(code) { + min-width: 20em; + } #common-content { display: flex; flex-direction: row-reverse; @@ -206,7 +210,7 @@ pre { } } -@media not all and (min-width: 700px) { +@media not all and (min-width: 800px) { #content-right .info-float { float: none; width: 100% !important; diff --git a/resources/markdown.css b/resources/markdown.css index 3637114..06d79ec 100644 --- a/resources/markdown.css +++ b/resources/markdown.css @@ -4295,7 +4295,7 @@ html .md-typeset .footnote-ref{ padding:0 1.1764705882em } .highlight span.filename{ - background-color:var(--md-code-bg-color); + background-color:var(--md-default-fg-color--lighter); border-bottom:.05rem solid var(--md-default-fg-color--lightest); border-top-left-radius:.1rem; border-top-right-radius:.1rem; @@ -4374,8 +4374,6 @@ html .md-typeset .footnote-ref{ background-color:var(--md-code-bg-color); border-bottom-left-radius:.1rem; border-top-left-radius:.1rem; - font-size:.85em; - padding:.7720588235em 0 .7720588235em 1.1764705882em; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; diff --git a/resources/submission.scss b/resources/submission.scss index ba07515..52e945a 100644 --- a/resources/submission.scss +++ b/resources/submission.scss @@ -155,48 +155,6 @@ label[for="language"], label[for="status"] { color: #555; } -.source-ln { - color: gray; - border-right: 1px solid gray; - padding-right: 5px; - text-align: right; - - a { - color: gray; - display: block; - - &:hover { - text-decoration: underline; - } - - &::before { - display: block; - content: " "; - margin-top: -50px; - height: 50px; - visibility: hidden; - } - } -} - -.source-code pre, .source-ln pre { - margin: 0; - padding: 0; - white-space: pre; -} - -.source-code { - padding-left: 15px; - width: 100%; -} - -.source-wrap { - overflow-x: auto; - padding: 1em; - border-radius: 10px; - border: double 4px darkgray; -} - .statistics-table { .status { font-weight: bold; diff --git a/resources/widgets.scss b/resources/widgets.scss index bcc1e90..9f29acc 100644 --- a/resources/widgets.scss +++ b/resources/widgets.scss @@ -200,30 +200,26 @@ input { // Bootstrap-y copy button .btn-clipboard { - top: 0; - right: 0; + top: -5px; + right: -8px; display: block; font-size: 12px; - color: #767676; cursor: pointer; - background-color: #FFF; - border: 1px solid #E1E1E8; - border-radius: 0 $widget_border_radius; position: absolute; padding: 5px 8px; + font-family: system-ui, "Noto Sans"; + + &:hover { + border-radius: $widget_border_radius; + border: 1px solid #E1E1E8; + background-color: #FFF; + } } .copy-clipboard { position: relative; } -.md-typeset .admonition .btn-clipboard, -.md-typeset details .btn-clipboard { - right: -0.6rem; - border-radius: 0 0 0 4px; -} - - // Bootstrap-y tabs .ul_tab_a_active { color: $theme_color; diff --git a/templates/base.html b/templates/base.html index f500306..1cd66d9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -50,9 +50,7 @@ {% compress css %} - {% if PYGMENT_THEME %} - - {% endif %}{% if INLINE_FONTAWESOME %} + {% if INLINE_FONTAWESOME %} {% endif %} diff --git a/templates/chat/chat_js.html b/templates/chat/chat_js.html index f09ca45..a843e5a 100644 --- a/templates/chat/chat_js.html +++ b/templates/chat/chat_js.html @@ -113,6 +113,7 @@ $('#chat-box').scrollTop($('#chat-box')[0].scrollHeight); register_time($('.time-with-rel')); MathJax.typeset(); + populateCopyButton(); merge_authors(); } @@ -167,6 +168,7 @@ add_new_message(message, room, true); } MathJax.typeset(); + populateCopyButton(); register_time($('.time-with-rel')); remove_unread_current_user(); merge_authors(); diff --git a/templates/common-content.html b/templates/common-content.html index 0ffb0ae..07a03b5 100644 --- a/templates/common-content.html +++ b/templates/common-content.html @@ -21,34 +21,6 @@ info_float.width(container.width()); } } - - // TODO: remove this - var copyButton; - $('pre code').each(function () { - $(this).parent().before($('
', {'class': 'copy-clipboard'}) - .append(copyButton = $('', { - 'class': 'btn-clipboard', - 'data-clipboard-text': $(this).text(), - 'title': 'Click to copy' - }).text('Copy'))); - - $(copyButton.get(0)).mouseleave(function () { - $(this).attr('class', 'btn-clipboard'); - $(this).removeAttr('aria-label'); - }); - - var curClipboard = new Clipboard(copyButton.get(0)); - - curClipboard.on('success', function (e) { - e.clearSelection(); - showTooltip(e.trigger, 'Copied!'); - }); - - curClipboard.on('error', function (e) { - showTooltip(e.trigger, fallbackMessage(e.action)); - }); - - }); }); {% endcompress %} diff --git a/templates/contest/tag-ajax.html b/templates/contest/tag-ajax.html index b027b89..5e0961b 100644 --- a/templates/contest/tag-ajax.html +++ b/templates/contest/tag-ajax.html @@ -4,4 +4,4 @@
{% endif %} -{{ tag.description|markdown }} +{{ tag.description }} diff --git a/templates/markdown_editor/markdown_editor.html b/templates/markdown_editor/markdown_editor.html index c56a6c5..5495d24 100644 --- a/templates/markdown_editor/markdown_editor.html +++ b/templates/markdown_editor/markdown_editor.html @@ -44,15 +44,13 @@ $.ajax({ url: "{{url('blog_preview')}}", type: 'POST', - headers: { - 'X-CSRFToken': csrfToken, // Include the CSRF token in the headers - }, data: { preview: $(this).val() }, success: function(data) { $('#display').html(data); MathJax.typeset(); + populateCopyButton(); }, error: function(error) { alert(error); diff --git a/templates/problem/yaml.html b/templates/problem/yaml.html index 1a7b821..33c703f 100644 --- a/templates/problem/yaml.html +++ b/templates/problem/yaml.html @@ -1,20 +1,5 @@ {% extends "base.html" %} {% block body %} -
- - - - - -
-
- {% for line in raw_source.split('\n') %} - -
{{ loop.index }}
-
- {% endfor %} -
-
{{ highlighted_source }}
-
+ {{ highlighted_source }} {% endblock %} \ No newline at end of file diff --git a/templates/submission/internal-error-message.html b/templates/submission/internal-error-message.html index 5ab1edc..f47c50a 100644 --- a/templates/submission/internal-error-message.html +++ b/templates/submission/internal-error-message.html @@ -13,6 +13,6 @@ {% if request.profile.id in submission.problem.editor_ids or perms.judge.edit_all_problem %}

{{ _('Error information') }}

- {{ submission.error|highlight('pytb', linenos=False) }} + {{ submission.error|highlight('pytb', linenos=True) }} {% endif %} {% endif %} \ No newline at end of file diff --git a/templates/submission/source.html b/templates/submission/source.html deleted file mode 100644 index d41b3a0..0000000 --- a/templates/submission/source.html +++ /dev/null @@ -1,64 +0,0 @@ -{% extends "submission/info-base.html" %} -{% block media %} - -{% endblock %} - -{% block body %} -
-
- - - {% if request.user == submission.user.user or perms.judge.resubmit_other %} - - {% endif %} - {% if perms.judge.rejudge_submission %} -
-
- {% csrf_token %} - {{ _('Rejudge') }} - - -
-
- {% endif %} -
-
-
-
- - - - - -
-
- {% for line in raw_source.split('\n') %} - -
{{ loop.index }}
-
- {% endfor %} -
-
{{ highlighted_source }}
-
-{% endblock %} diff --git a/templates/submission/status.html b/templates/submission/status.html index 2bbc070..16f773b 100644 --- a/templates/submission/status.html +++ b/templates/submission/status.html @@ -135,7 +135,7 @@ {% block body %}

- {% if request.user == submission.user.user or perms.judge.resubmit_other %} + {% if request.profile == submission.user or perms.judge.resubmit_other %} {% endif %} {% if perms.judge.rejudge_submission %} @@ -151,21 +151,8 @@

{{_('Source code')}}

-