mirror of
https://github.com/discourse/discourse.git
synced 2025-04-25 10:04:27 +08:00
FEATURE: add emoji rich editor extension (#31144)
Continues the work done on
https://github.com/discourse/discourse/pull/30815.
Adds an `emoji` node, its Markdown serializing/parsing logic, and input
rules for `:emoji:`s and `:-)` emoticon replacements.
[prosemirror-markdown's
heading](99b6f0a6c3/src/schema.ts (L30)
)
only allows `(text | image)*` content, we override it to allow `inline*`
to be compatible with all our inline (including emoji) nodes.
This commit is contained in:
parent
5a21f74716
commit
6168636de8
@ -56,7 +56,7 @@ export async function parseMentions(markdown, options) {
|
||||
return await withEngine("parseMentions", markdown, options);
|
||||
}
|
||||
|
||||
function emojiOptions() {
|
||||
export function emojiOptions() {
|
||||
let siteSettings = helperContext().siteSettings;
|
||||
let context = helperContext();
|
||||
if (!siteSettings.enable_emoji) {
|
||||
|
@ -0,0 +1,98 @@
|
||||
import { buildEmojiUrl, emojiExists, isCustomEmoji } from "pretty-text/emoji";
|
||||
import { translations } from "pretty-text/emoji/data";
|
||||
import escapeRegExp from "discourse/lib/escape-regexp";
|
||||
import { emojiOptions } from "discourse/lib/text";
|
||||
import { isBoundary } from "discourse/static/prosemirror/lib/markdown-it";
|
||||
|
||||
/** @type {RichEditorExtension} */
|
||||
const extension = {
|
||||
nodeSpec: {
|
||||
emoji: {
|
||||
attrs: { code: {} },
|
||||
inline: true,
|
||||
group: "inline",
|
||||
draggable: true,
|
||||
selectable: false,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: "img.emoji",
|
||||
getAttrs: (dom) => {
|
||||
return { code: dom.getAttribute("alt").replace(/:/g, "") };
|
||||
},
|
||||
priority: 60,
|
||||
},
|
||||
],
|
||||
toDOM: (node) => {
|
||||
const opts = emojiOptions();
|
||||
const code = node.attrs.code.toLowerCase();
|
||||
const title = `:${code}:`;
|
||||
const src = buildEmojiUrl(code, opts);
|
||||
|
||||
return [
|
||||
"img",
|
||||
{
|
||||
class: isCustomEmoji(code, opts) ? "emoji emoji-custom" : "emoji",
|
||||
alt: title,
|
||||
title,
|
||||
src,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
inputRules: [
|
||||
{
|
||||
match: /(^|\W):([^:]+):$/,
|
||||
handler: (state, match, start, end) => {
|
||||
if (emojiExists(match[2])) {
|
||||
const emojiStart = start + match[1].length;
|
||||
return state.tr.replaceWith(
|
||||
emojiStart,
|
||||
end,
|
||||
state.schema.nodes.emoji.create({ code: match[2] })
|
||||
);
|
||||
}
|
||||
},
|
||||
options: { undoable: false },
|
||||
},
|
||||
{
|
||||
match: new RegExp(
|
||||
"(^|\\W)(" +
|
||||
Object.keys(translations).map(escapeRegExp).join("|") +
|
||||
") $"
|
||||
),
|
||||
handler: (state, match, start, end) => {
|
||||
const emojiStart = start + match[1].length;
|
||||
return state.tr
|
||||
.replaceWith(
|
||||
emojiStart,
|
||||
end,
|
||||
state.schema.nodes.emoji.create({ code: translations[match[2]] })
|
||||
)
|
||||
.insertText(" ");
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
parse: {
|
||||
emoji: {
|
||||
node: "emoji",
|
||||
getAttrs: (token) => ({
|
||||
code: token.attrGet("alt").slice(1, -1),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
serializeNode: {
|
||||
emoji(state, node) {
|
||||
if (!isBoundary(state.out, state.out.length - 1)) {
|
||||
state.write(" ");
|
||||
}
|
||||
|
||||
state.write(`:${node.attrs.code}:`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default extension;
|
@ -0,0 +1,25 @@
|
||||
/** @type {RichEditorExtension} */
|
||||
const extension = {
|
||||
nodeSpec: {
|
||||
heading: {
|
||||
attrs: { level: { default: 1 } },
|
||||
// Overriding ProseMirror's default to allow inline content
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
defining: true,
|
||||
parseDOM: [
|
||||
{ tag: "h1", attrs: { level: 1 } },
|
||||
{ tag: "h2", attrs: { level: 2 } },
|
||||
{ tag: "h3", attrs: { level: 3 } },
|
||||
{ tag: "h4", attrs: { level: 4 } },
|
||||
{ tag: "h5", attrs: { level: 5 } },
|
||||
{ tag: "h6", attrs: { level: 6 } },
|
||||
],
|
||||
toDOM(node) {
|
||||
return ["h" + node.attrs.level, 0];
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default extension;
|
@ -1,5 +1,7 @@
|
||||
import { registerRichEditorExtension } from "discourse/lib/composer/rich-editor-extensions";
|
||||
import emoji from "./emoji";
|
||||
import hashtag from "./hashtag";
|
||||
import heading from "./heading";
|
||||
import mention from "./mention";
|
||||
import underline from "./underline";
|
||||
|
||||
@ -9,7 +11,7 @@ import underline from "./underline";
|
||||
*
|
||||
* @type {RichEditorExtension[]}
|
||||
*/
|
||||
const defaultExtensions = [hashtag, mention, underline];
|
||||
const defaultExtensions = [emoji, heading, hashtag, mention, underline];
|
||||
|
||||
defaultExtensions.forEach(registerRichEditorExtension);
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper";
|
||||
|
||||
module(
|
||||
"Integration | Component | prosemirror-editor - emoji extension",
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
const testCases = {
|
||||
emoji: [
|
||||
[
|
||||
"Hey :tada:!",
|
||||
'<p>Hey <img class="emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=13" contenteditable="false" draggable="true">!</p>',
|
||||
"Hey :tada:!",
|
||||
],
|
||||
],
|
||||
"emoji in heading": [
|
||||
[
|
||||
"# Heading :information_source:",
|
||||
'<h1>Heading <img class="emoji" alt=":information_source:" title=":information_source:" src="/images/emoji/twitter/information_source.png?v=13" contenteditable="false" draggable="true"></h1>',
|
||||
"# Heading :information_source:",
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
Object.entries(testCases).forEach(([name, tests]) => {
|
||||
tests.forEach(([markdown, expectedHtml, expectedMarkdown]) => {
|
||||
test(name, async function (assert) {
|
||||
this.siteSettings.rich_editor = true;
|
||||
|
||||
await testMarkdown(assert, markdown, expectedHtml, expectedMarkdown);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user