Implement markdown emoji, youtube, clipboard
This commit is contained in:
parent
5e72b472e6
commit
e923d1b2fe
12 changed files with 381 additions and 27 deletions
|
@ -4,6 +4,8 @@ from django.utils.html import escape
|
|||
from bs4 import BeautifulSoup
|
||||
from pymdownx import superfences
|
||||
|
||||
from judge.markdown_extensions import YouTubeExtension, EmoticonExtension
|
||||
|
||||
|
||||
EXTENSIONS = [
|
||||
"pymdownx.arithmatex",
|
||||
|
@ -22,6 +24,8 @@ EXTENSIONS = [
|
|||
"markdown.extensions.admonition",
|
||||
"nl2br",
|
||||
"mdx_breakless_lists",
|
||||
YouTubeExtension(),
|
||||
EmoticonExtension(),
|
||||
]
|
||||
|
||||
EXTENSION_CONFIGS = {
|
||||
|
|
2
judge/markdown_extensions/__init__.py
Normal file
2
judge/markdown_extensions/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .youtube import YouTubeExtension
|
||||
from .emoticon import EmoticonExtension
|
111
judge/markdown_extensions/emoticon.py
Normal file
111
judge/markdown_extensions/emoticon.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
import markdown
|
||||
from markdown.extensions import Extension
|
||||
from markdown.inlinepatterns import InlineProcessor
|
||||
import xml.etree.ElementTree as etree
|
||||
import re
|
||||
|
||||
EMOTICON_EMOJI_MAP = {
|
||||
":D": "\U0001F603", # Smiling Face with Open Mouth
|
||||
":)": "\U0001F642", # Slightly Smiling Face
|
||||
":-)": "\U0001F642", # Slightly Smiling Face with Nose
|
||||
":(": "\U0001F641", # Slightly Frowning Face
|
||||
":-(": "\U0001F641", # Slightly Frowning Face with Nose
|
||||
";)": "\U0001F609", # Winking Face
|
||||
";-)": "\U0001F609", # Winking Face with Nose
|
||||
":P": "\U0001F61B", # Face with Tongue
|
||||
":-P": "\U0001F61B", # Face with Tongue and Nose
|
||||
":p": "\U0001F61B", # Face with Tongue
|
||||
":-p": "\U0001F61B", # Face with Tongue and Nose
|
||||
";P": "\U0001F61C", # Winking Face with Tongue
|
||||
";-P": "\U0001F61C", # Winking Face with Tongue and Nose
|
||||
";p": "\U0001F61C", # Winking Face with Tongue
|
||||
";-p": "\U0001F61C", # Winking Face with Tongue and Nose
|
||||
":'(": "\U0001F622", # Crying Face
|
||||
":o": "\U0001F62E", # Face with Open Mouth
|
||||
":-o": "\U0001F62E", # Face with Open Mouth and Nose
|
||||
":O": "\U0001F62E", # Face with Open Mouth
|
||||
":-O": "\U0001F62E", # Face with Open Mouth and Nose
|
||||
":-0": "\U0001F62E", # Face with Open Mouth and Nose
|
||||
">:(": "\U0001F620", # Angry Face
|
||||
">:-(": "\U0001F620", # Angry Face with Nose
|
||||
">:)": "\U0001F608", # Smiling Face with Horns
|
||||
">:-)": "\U0001F608", # Smiling Face with Horns and Nose
|
||||
"XD": "\U0001F606", # Grinning Squinting Face
|
||||
"xD": "\U0001F606", # Grinning Squinting Face
|
||||
"B)": "\U0001F60E", # Smiling Face with Sunglasses
|
||||
"B-)": "\U0001F60E", # Smiling Face with Sunglasses and Nose
|
||||
"O:)": "\U0001F607", # Smiling Face with Halo
|
||||
"O:-)": "\U0001F607", # Smiling Face with Halo and Nose
|
||||
"0:)": "\U0001F607", # Smiling Face with Halo
|
||||
"0:-)": "\U0001F607", # Smiling Face with Halo and Nose
|
||||
">:P": "\U0001F92A", # Zany Face (sticking out tongue and winking)
|
||||
">:-P": "\U0001F92A", # Zany Face with Nose
|
||||
">:p": "\U0001F92A", # Zany Face (sticking out tongue and winking)
|
||||
">:-p": "\U0001F92A", # Zany Face with Nose
|
||||
":/": "\U0001F615", # Confused Face
|
||||
":-/": "\U0001F615", # Confused Face with Nose
|
||||
":\\": "\U0001F615", # Confused Face
|
||||
":-\\": "\U0001F615", # Confused Face with Nose
|
||||
"3:)": "\U0001F608", # Smiling Face with Horns
|
||||
"3:-)": "\U0001F608", # Smiling Face with Horns and Nose
|
||||
"<3": "\u2764\uFE0F", # Red Heart
|
||||
"</3": "\U0001F494", # Broken Heart
|
||||
":*": "\U0001F618", # Face Blowing a Kiss
|
||||
":-*": "\U0001F618", # Face Blowing a Kiss with Nose
|
||||
";P": "\U0001F61C", # Winking Face with Tongue
|
||||
";-P": "\U0001F61C",
|
||||
">:P": "\U0001F61D", # Face with Stuck-Out Tongue and Tightly-Closed Eyes
|
||||
":-/": "\U0001F615", # Confused Face
|
||||
":/": "\U0001F615",
|
||||
":\\": "\U0001F615",
|
||||
":-\\": "\U0001F615",
|
||||
":|": "\U0001F610", # Neutral Face
|
||||
":-|": "\U0001F610",
|
||||
"8)": "\U0001F60E", # Smiling Face with Sunglasses
|
||||
"8-)": "\U0001F60E",
|
||||
"O:)": "\U0001F607", # Smiling Face with Halo
|
||||
"O:-)": "\U0001F607",
|
||||
":3": "\U0001F60A", # Smiling Face with Smiling Eyes
|
||||
"^.^": "\U0001F60A",
|
||||
"-_-": "\U0001F611", # Expressionless Face
|
||||
"T_T": "\U0001F62D", # Loudly Crying Face
|
||||
"T.T": "\U0001F62D",
|
||||
">.<": "\U0001F623", # Persevering Face
|
||||
"x_x": "\U0001F635", # Dizzy Face
|
||||
"X_X": "\U0001F635",
|
||||
":]": "\U0001F600", # Grinning Face
|
||||
":[": "\U0001F641", # Slightly Frowning Face
|
||||
"=]": "\U0001F600",
|
||||
"=[": "\U0001F641",
|
||||
"D:<": "\U0001F621", # Pouting Face
|
||||
"D:": "\U0001F629", # Weary Face
|
||||
"D=": "\U0001F6AB", # No Entry Sign (sometimes used to denote dismay or frustration)
|
||||
":'D": "\U0001F602", # Face with Tears of Joy
|
||||
"D':": "\U0001F625", # Disappointed but Relieved Face
|
||||
"D8": "\U0001F631", # Face Screaming in Fear
|
||||
"-.-": "\U0001F644", # Face with Rolling Eyes
|
||||
"-_-;": "\U0001F612", # Unamused
|
||||
}
|
||||
|
||||
|
||||
class EmoticonEmojiInlineProcessor(InlineProcessor):
|
||||
def handleMatch(self, m, data):
|
||||
emoticon = m.group(1)
|
||||
emoji = EMOTICON_EMOJI_MAP.get(emoticon, "")
|
||||
if emoji:
|
||||
el = etree.Element("span")
|
||||
el.text = markdown.util.AtomicString(emoji)
|
||||
el.set("class", "big-emoji")
|
||||
return el, m.start(0), m.end(0)
|
||||
else:
|
||||
return None, m.start(0), m.end(0)
|
||||
|
||||
|
||||
class EmoticonExtension(Extension):
|
||||
def extendMarkdown(self, md):
|
||||
# Create the regex pattern to match any emoticon in the map
|
||||
emoticon_pattern = (
|
||||
r"(" + "|".join(map(re.escape, EMOTICON_EMOJI_MAP.keys())) + r")"
|
||||
)
|
||||
emoticon_processor = EmoticonEmojiInlineProcessor(emoticon_pattern, md)
|
||||
md.inlinePatterns.register(emoticon_processor, "emoticon_to_emoji", 1)
|
36
judge/markdown_extensions/youtube.py
Normal file
36
judge/markdown_extensions/youtube.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
import markdown
|
||||
from markdown.inlinepatterns import InlineProcessor
|
||||
from markdown.extensions import Extension
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
YOUTUBE_REGEX = (
|
||||
r"(https?://)?(www\.)?" "(youtube\.com/watch\?v=|youtu\.be/)" "([\w-]+)(&[\w=]*)?"
|
||||
)
|
||||
|
||||
|
||||
class YouTubeEmbedProcessor(InlineProcessor):
|
||||
def handleMatch(self, m, data):
|
||||
youtube_id = m.group(4)
|
||||
if not youtube_id:
|
||||
return None, None, None
|
||||
|
||||
# Create an iframe element with the YouTube embed URL
|
||||
iframe = etree.Element("iframe")
|
||||
iframe.set("width", "100%")
|
||||
iframe.set("height", "360")
|
||||
iframe.set("src", f"https://www.youtube.com/embed/{youtube_id}")
|
||||
iframe.set("frameborder", "0")
|
||||
iframe.set("allowfullscreen", "true")
|
||||
center = etree.Element("center")
|
||||
center.append(iframe)
|
||||
|
||||
# Return the iframe as the element to replace the match, along with the start and end indices
|
||||
return center, m.start(0), m.end(0)
|
||||
|
||||
|
||||
class YouTubeExtension(Extension):
|
||||
def extendMarkdown(self, md):
|
||||
# Create the YouTube link pattern
|
||||
YOUTUBE_PATTERN = YouTubeEmbedProcessor(YOUTUBE_REGEX, md)
|
||||
# Register the pattern to apply the YouTubeEmbedProcessor
|
||||
md.inlinePatterns.register(YOUTUBE_PATTERN, "youtube", 175)
|
|
@ -726,7 +726,7 @@ noscript #noscript {
|
|||
}
|
||||
|
||||
.featherlight {
|
||||
z-index: 1000 !important;
|
||||
z-index: 1001 !important;
|
||||
}
|
||||
|
||||
// @media (max-width: 500px) {
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
margin-top: 1em;
|
||||
}
|
||||
.big-emoji {
|
||||
font-size: 16px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
#chat-online {
|
||||
border-right: 1px solid #ccc;
|
||||
|
|
|
@ -311,17 +311,49 @@ function populateCopyButton() {
|
|||
});
|
||||
}
|
||||
|
||||
function register_markdown_editors() {
|
||||
if (!("Markdown" in window)) {
|
||||
return;
|
||||
}
|
||||
$('textarea.wmd-input').each(function() {
|
||||
let id = this.id.substr(9); // remove prefix "wmd-input"
|
||||
var $buttonBar = $(this).prevAll('div[id^="wmd-button-bar"]').first();
|
||||
if (!$buttonBar.length || !$buttonBar.html().trim()) {
|
||||
let converter = new Markdown.Converter();
|
||||
let editor = new Markdown.Editor(converter, id);
|
||||
editor.run();
|
||||
function register_copy_clipboard($elements, callback) {
|
||||
$elements.on('paste', function(event) {
|
||||
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
||||
for (const index in items) {
|
||||
const item = items[index];
|
||||
if (item.kind === 'file' && item.type.indexOf('image') !== -1) {
|
||||
const blob = item.getAsFile();
|
||||
const formData = new FormData();
|
||||
formData.append('image', blob);
|
||||
|
||||
$(this).prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: '/pagedown/image-upload/',
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(data) {
|
||||
// Assuming the server returns the URL of the image
|
||||
const imageUrl = data.url;
|
||||
const editor = $(event.target); // Get the textarea where the event was triggered
|
||||
let currentMarkdown = editor.val();
|
||||
const markdownImageText = '![](' + imageUrl + ')'; // Markdown for an image
|
||||
|
||||
if (currentMarkdown) currentMarkdown += "\n";
|
||||
currentMarkdown += markdownImageText;
|
||||
|
||||
editor.val(currentMarkdown);
|
||||
callback?.();
|
||||
},
|
||||
error: function() {
|
||||
alert('There was an error uploading the image.');
|
||||
},
|
||||
complete: () => {
|
||||
// Re-enable the editor
|
||||
$(this).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
|
||||
// We only handle the first image in the clipboard data
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -399,9 +431,7 @@ function onWindowReady() {
|
|||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
register_markdown_editors();
|
||||
}, 100);
|
||||
register_copy_clipboard($('textarea.wmd-input'));
|
||||
|
||||
$('form').submit(function (evt) {
|
||||
// Prevent multiple submissions of forms, see #565
|
||||
|
|
|
@ -4129,17 +4129,20 @@ html .md-typeset .footnote-ref{
|
|||
.md-typeset :-webkit-any(.emojione,.twemoji,.gemoji){
|
||||
display:inline-flex;
|
||||
height:1.125em;
|
||||
vertical-align:text-top
|
||||
vertical-align:text-top;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.md-typeset :-moz-any(.emojione,.twemoji,.gemoji){
|
||||
display:inline-flex;
|
||||
height:1.125em;
|
||||
vertical-align:text-top
|
||||
vertical-align:text-top;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.md-typeset :is(.emojione,.twemoji,.gemoji){
|
||||
display:inline-flex;
|
||||
height:1.125em;
|
||||
vertical-align:text-top
|
||||
vertical-align:text-top;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.md-typeset :-webkit-any(.emojione,.twemoji,.gemoji) svg{
|
||||
fill:currentcolor;
|
||||
|
@ -4149,12 +4152,14 @@ html .md-typeset .footnote-ref{
|
|||
.md-typeset :-moz-any(.emojione,.twemoji,.gemoji) svg{
|
||||
fill:currentcolor;
|
||||
max-height:100%;
|
||||
width:1.125em
|
||||
width:1.125em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.md-typeset :is(.emojione,.twemoji,.gemoji) svg{
|
||||
fill:currentcolor;
|
||||
max-height:100%;
|
||||
width:1.125em
|
||||
width:1.125em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.highlight :-webkit-any(.o,.ow){
|
||||
color:var(--md-code-hl-operator-color)
|
||||
|
|
158
resources/pagedown_init.js
Normal file
158
resources/pagedown_init.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
var DjangoPagedown = DjangoPagedown | {};
|
||||
|
||||
DjangoPagedown = (function() {
|
||||
var converter = Markdown.getSanitizingConverter();
|
||||
var editors = {};
|
||||
var elements;
|
||||
|
||||
Markdown.Extra.init(converter, {
|
||||
extensions: "all"
|
||||
});
|
||||
|
||||
var createEditor = function(element) {
|
||||
var input = element.getElementsByClassName("wmd-input")[0];
|
||||
if (input === undefined) {
|
||||
return
|
||||
}
|
||||
var id = input.id.substr(9);
|
||||
if (!editors.hasOwnProperty(id)) {
|
||||
var editor = new Markdown.Editor(converter, id, {});
|
||||
|
||||
// Handle image upload
|
||||
if (element.classList.contains("image-upload-enabled")) {
|
||||
var upload = element.getElementsByClassName("pagedown-image-upload")[0];
|
||||
var url = upload.getElementsByClassName("url-input")[0];
|
||||
var file = upload.getElementsByClassName("file-input")[0];
|
||||
var cancel = upload.getElementsByClassName("deletelink")[0];
|
||||
var submit = upload.getElementsByClassName("submit-input")[0];
|
||||
var loading = upload.getElementsByClassName("submit-loading")[0];
|
||||
|
||||
var close = function(value, callback = undefined) {
|
||||
upload.classList.remove("show");
|
||||
url.value = "";
|
||||
file.value = "";
|
||||
document.removeEventListener('click', outsideClickListener);
|
||||
if (callback) callback(value);
|
||||
};
|
||||
|
||||
var outsideClickListener = function(event) {
|
||||
if (!upload.contains(event.target) && upload.classList.contains("show")) {
|
||||
cancel.click();
|
||||
}
|
||||
};
|
||||
|
||||
editor.hooks.set("insertImageDialog", function(callback) {
|
||||
upload.classList.add("show");
|
||||
|
||||
setTimeout(function() {
|
||||
document.addEventListener('click', outsideClickListener);
|
||||
}, 0);
|
||||
|
||||
cancel.addEventListener(
|
||||
"click",
|
||||
function(event) {
|
||||
close(null, callback);
|
||||
event.preventDefault();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
submit.addEventListener(
|
||||
"click",
|
||||
function() {
|
||||
// Regular URL
|
||||
if (url.value.length > 0) {
|
||||
close(url.value, callback);
|
||||
}
|
||||
// File upload
|
||||
else if (file.files.length > 0) {
|
||||
loading.classList.add("show");
|
||||
submit.classList.remove("show");
|
||||
|
||||
var data = new FormData();
|
||||
var xhr = new XMLHttpRequest();
|
||||
data.append("image", file.files[0]);
|
||||
xhr.open("POST", file.dataset.action, true);
|
||||
xhr.addEventListener(
|
||||
"load",
|
||||
function() {
|
||||
loading.classList.remove("show");
|
||||
submit.classList.add("show");
|
||||
|
||||
if (xhr.status !== 200) {
|
||||
alert(xhr.statusText);
|
||||
} else {
|
||||
var response = JSON.parse(xhr.response);
|
||||
if (response.success) {
|
||||
close(response.url, callback);
|
||||
} else {
|
||||
if (response.error) {
|
||||
var error = "";
|
||||
for (var key in response.error) {
|
||||
if (response.error.hasOwnProperty(key)) {
|
||||
error += key + ":" + response.error[key];
|
||||
}
|
||||
}
|
||||
alert(error);
|
||||
}
|
||||
close(null, callback);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
once: true
|
||||
}
|
||||
);
|
||||
xhr.send(data);
|
||||
} else {
|
||||
// Nothing
|
||||
close(null, callback);
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
editor.run();
|
||||
editors[id] = editor;
|
||||
}
|
||||
};
|
||||
|
||||
var destroyEditor = function(element) {
|
||||
var input = element.getElementsByClassName("wmd-input")[0];
|
||||
if (input === undefined) {
|
||||
return
|
||||
}
|
||||
var id = input.id.substr(9);
|
||||
if (editors.hasOwnProperty(id)) {
|
||||
delete editors[id];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var init = function() {
|
||||
elements = document.getElementsByClassName("wmd-wrapper");
|
||||
for (var i = 0; i < elements.length; ++i) {
|
||||
createEditor(elements[i]);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: function() {
|
||||
return init();
|
||||
},
|
||||
createEditor: function(element) {
|
||||
return createEditor(element);
|
||||
},
|
||||
destroyEditor: function(element) {
|
||||
return destroyEditor(element);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.onload = DjangoPagedown.init;
|
|
@ -585,5 +585,8 @@
|
|||
});
|
||||
|
||||
$('#submit-button').on('click', submit_chat);
|
||||
register_copy_clipboard($("#chat-input"), () => {
|
||||
$('#chat-input').trigger('input');
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -211,14 +211,15 @@
|
|||
$comments.find('a.edit-link').featherlight({
|
||||
afterOpen: function () {
|
||||
register_dmmd_preview($('#id-edit-comment-body-preview'));
|
||||
register_markdown_editors();
|
||||
if ('DjangoPagedown' in window) {
|
||||
DjangoPagedown.createEditor(
|
||||
$('#wmd-input-id-edit-comment-body').closest('.wmd-wrapper')[0]
|
||||
);
|
||||
register_copy_clipboard($('#wmd-input-id-edit-comment-body'));
|
||||
var $wmd = $('.featherlight .wmd-wrapper');
|
||||
if ($wmd.length) {
|
||||
if ('MathJax' in window) {
|
||||
var preview = $('.featherlight div.wmd-preview')[0];
|
||||
renderKatex(preview);
|
||||
}
|
||||
var preview = $('.featherlight div.wmd-preview')[0];
|
||||
renderKatex(preview);
|
||||
}
|
||||
}
|
||||
$('#comment-edit').submit(function (event) {
|
||||
|
@ -242,6 +243,11 @@
|
|||
});
|
||||
});
|
||||
},
|
||||
beforeClose: function() {
|
||||
DjangoPagedown.destroyEditor(
|
||||
$('#wmd-input-id-edit-comment-body').closest('.wmd-wrapper')[0]
|
||||
);
|
||||
},
|
||||
variant: 'featherlight-edit'
|
||||
});
|
||||
|
||||
|
|
|
@ -64,7 +64,6 @@
|
|||
populateCopyButton();
|
||||
},
|
||||
error: function(error) {
|
||||
alert(error);
|
||||
console.log(error.message);
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue