Implement markdown emoji, youtube, clipboard

This commit is contained in:
cuom1999 2024-03-19 23:51:12 -05:00
parent 5e72b472e6
commit e923d1b2fe
12 changed files with 381 additions and 27 deletions

View file

@ -4,6 +4,8 @@ from django.utils.html import escape
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from pymdownx import superfences from pymdownx import superfences
from judge.markdown_extensions import YouTubeExtension, EmoticonExtension
EXTENSIONS = [ EXTENSIONS = [
"pymdownx.arithmatex", "pymdownx.arithmatex",
@ -22,6 +24,8 @@ EXTENSIONS = [
"markdown.extensions.admonition", "markdown.extensions.admonition",
"nl2br", "nl2br",
"mdx_breakless_lists", "mdx_breakless_lists",
YouTubeExtension(),
EmoticonExtension(),
] ]
EXTENSION_CONFIGS = { EXTENSION_CONFIGS = {

View file

@ -0,0 +1,2 @@
from .youtube import YouTubeExtension
from .emoticon import EmoticonExtension

View 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)

View 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)

View file

@ -726,7 +726,7 @@ noscript #noscript {
} }
.featherlight { .featherlight {
z-index: 1000 !important; z-index: 1001 !important;
} }
// @media (max-width: 500px) { // @media (max-width: 500px) {

View file

@ -27,7 +27,7 @@
margin-top: 1em; margin-top: 1em;
} }
.big-emoji { .big-emoji {
font-size: 16px; font-size: 1.2em;
} }
#chat-online { #chat-online {
border-right: 1px solid #ccc; border-right: 1px solid #ccc;

View file

@ -311,17 +311,49 @@ function populateCopyButton() {
}); });
} }
function register_markdown_editors() { function register_copy_clipboard($elements, callback) {
if (!("Markdown" in window)) { $elements.on('paste', function(event) {
return; const items = (event.clipboardData || event.originalEvent.clipboardData).items;
} for (const index in items) {
$('textarea.wmd-input').each(function() { const item = items[index];
let id = this.id.substr(9); // remove prefix "wmd-input" if (item.kind === 'file' && item.type.indexOf('image') !== -1) {
var $buttonBar = $(this).prevAll('div[id^="wmd-button-bar"]').first(); const blob = item.getAsFile();
if (!$buttonBar.length || !$buttonBar.html().trim()) { const formData = new FormData();
let converter = new Markdown.Converter(); formData.append('image', blob);
let editor = new Markdown.Editor(converter, id);
editor.run(); $(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_copy_clipboard($('textarea.wmd-input'));
register_markdown_editors();
}, 100);
$('form').submit(function (evt) { $('form').submit(function (evt) {
// Prevent multiple submissions of forms, see #565 // Prevent multiple submissions of forms, see #565

View file

@ -4129,17 +4129,20 @@ html .md-typeset .footnote-ref{
.md-typeset :-webkit-any(.emojione,.twemoji,.gemoji){ .md-typeset :-webkit-any(.emojione,.twemoji,.gemoji){
display:inline-flex; display:inline-flex;
height:1.125em; height:1.125em;
vertical-align:text-top vertical-align:text-top;
font-size: 1.2em;
} }
.md-typeset :-moz-any(.emojione,.twemoji,.gemoji){ .md-typeset :-moz-any(.emojione,.twemoji,.gemoji){
display:inline-flex; display:inline-flex;
height:1.125em; height:1.125em;
vertical-align:text-top vertical-align:text-top;
font-size: 1.2em;
} }
.md-typeset :is(.emojione,.twemoji,.gemoji){ .md-typeset :is(.emojione,.twemoji,.gemoji){
display:inline-flex; display:inline-flex;
height:1.125em; height:1.125em;
vertical-align:text-top vertical-align:text-top;
font-size: 1.2em;
} }
.md-typeset :-webkit-any(.emojione,.twemoji,.gemoji) svg{ .md-typeset :-webkit-any(.emojione,.twemoji,.gemoji) svg{
fill:currentcolor; fill:currentcolor;
@ -4149,12 +4152,14 @@ html .md-typeset .footnote-ref{
.md-typeset :-moz-any(.emojione,.twemoji,.gemoji) svg{ .md-typeset :-moz-any(.emojione,.twemoji,.gemoji) svg{
fill:currentcolor; fill:currentcolor;
max-height:100%; max-height:100%;
width:1.125em width:1.125em;
font-size: 1.2em;
} }
.md-typeset :is(.emojione,.twemoji,.gemoji) svg{ .md-typeset :is(.emojione,.twemoji,.gemoji) svg{
fill:currentcolor; fill:currentcolor;
max-height:100%; max-height:100%;
width:1.125em width:1.125em;
font-size: 1.2em;
} }
.highlight :-webkit-any(.o,.ow){ .highlight :-webkit-any(.o,.ow){
color:var(--md-code-hl-operator-color) color:var(--md-code-hl-operator-color)

158
resources/pagedown_init.js Normal file
View 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;

View file

@ -585,5 +585,8 @@
}); });
$('#submit-button').on('click', submit_chat); $('#submit-button').on('click', submit_chat);
register_copy_clipboard($("#chat-input"), () => {
$('#chat-input').trigger('input');
});
}); });
</script> </script>

View file

@ -211,14 +211,15 @@
$comments.find('a.edit-link').featherlight({ $comments.find('a.edit-link').featherlight({
afterOpen: function () { afterOpen: function () {
register_dmmd_preview($('#id-edit-comment-body-preview')); register_dmmd_preview($('#id-edit-comment-body-preview'));
register_markdown_editors();
if ('DjangoPagedown' in window) { 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'); var $wmd = $('.featherlight .wmd-wrapper');
if ($wmd.length) { if ($wmd.length) {
if ('MathJax' in window) { var preview = $('.featherlight div.wmd-preview')[0];
var preview = $('.featherlight div.wmd-preview')[0]; renderKatex(preview);
renderKatex(preview);
}
} }
} }
$('#comment-edit').submit(function (event) { $('#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' variant: 'featherlight-edit'
}); });

View file

@ -64,7 +64,6 @@
populateCopyButton(); populateCopyButton();
}, },
error: function(error) { error: function(error) {
alert(error);
console.log(error.message); console.log(error.message);
} }
}) })