soapbox/app/soapbox/features/emoji/index.ts

229 wiersze
5.3 KiB
TypeScript
Czysty Zwykły widok Historia

2022-07-05 06:37:07 +00:00
import split from 'graphemesplit';
2022-07-04 20:30:35 +00:00
import unicodeMapping from './mapping';
2022-07-03 08:12:57 +00:00
import type { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from 'soapbox/features/emoji/data';
2022-07-05 03:11:46 +00:00
2022-07-05 03:45:01 +00:00
/*
* TODO: Consolate emoji object types
*
* There are five different emoji objects currently
* - emoji-mart's "onPickEmoji" handler
* - emoji-mart's custom emoji types
* - an Emoji type that is either NativeEmoji or CustomEmoji
* - a type inside redux's `store.custom_emoji` immutablejs
*
2022-07-05 03:45:01 +00:00
* there needs to be one type for the picker handler callback
* and one type for the emoji-mart data
* and one type that is used everywhere that the above two are converted into
*/
2022-07-05 03:59:39 +00:00
export interface CustomEmoji {
id: string
colons: string
custom: true
imageUrl: string
2022-07-05 03:11:46 +00:00
}
2022-07-05 03:59:39 +00:00
export interface NativeEmoji {
id: string
colons: string
custom?: boolean
unified: string
native: string
2022-07-05 03:11:46 +00:00
}
2022-07-05 03:59:39 +00:00
export type Emoji = CustomEmoji | NativeEmoji;
2022-07-05 03:11:46 +00:00
export function isCustomEmoji(emoji: Emoji): emoji is CustomEmoji {
return (emoji as CustomEmoji).imageUrl !== undefined;
}
export function isNativeEmoji(emoji: Emoji): emoji is NativeEmoji {
return (emoji as NativeEmoji).native !== undefined;
}
2022-07-03 08:12:57 +00:00
const isAlphaNumeric = (c: string) => {
const code = c.charCodeAt(0);
2022-07-04 20:30:35 +00:00
if (!(code > 47 && code < 58) && // numeric (0-9)
!(code > 64 && code < 91) && // upper alpha (A-Z)
2022-07-03 08:12:57 +00:00
!(code > 96 && code < 123)) { // lower alpha (a-z)
return false;
} else {
return true;
}
};
const validEmojiChar = (c: string) => {
return isAlphaNumeric(c)
|| c === '_'
2022-07-07 02:32:29 +00:00
|| c === '-'
|| c === '.';
2022-07-03 08:12:57 +00:00
};
const convertCustom = (shortname: string, filename: string) => {
return `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
};
const convertUnicode = (c: string) => {
const { unified, shortcode } = unicodeMapping[c];
2022-07-07 22:33:55 +00:00
return `<img draggable="false" class="emojione" alt="${c}" title=":${shortcode}:" src="/packs/emoji/${unified}.svg" />`;
2022-07-03 08:12:57 +00:00
};
2022-07-04 20:30:35 +00:00
const convertEmoji = (str: string, customEmojis: any) => {
2022-07-03 08:45:30 +00:00
if (str.length < 3) return str;
2022-07-03 08:12:57 +00:00
if (str in customEmojis) {
const emoji = customEmojis[str];
2022-07-04 20:30:35 +00:00
const filename = emoji.static_url;
2022-07-03 08:12:57 +00:00
if (filename?.length > 0) {
return convertCustom(str, filename);
}
}
2022-07-03 08:45:30 +00:00
return str;
2022-07-03 08:12:57 +00:00
};
2022-07-04 20:30:35 +00:00
export const emojifyText = (str: string, customEmojis = {}) => {
let buf = '';
2022-07-03 08:12:57 +00:00
let stack = '';
let open = false;
2022-07-06 03:47:59 +00:00
const clearStack = () => {
buf += stack;
open = false;
stack = '';
};
2022-07-09 13:59:26 +00:00
for (let c of split(str)) {
2022-07-09 23:32:34 +00:00
// convert FE0E selector to FE0F so it can be found in unimap
2022-07-09 13:59:26 +00:00
if (c.codePointAt(c.length - 1) === 65038) {
c = c.slice(0, -1) + String.fromCodePoint(65039);
}
2022-07-09 23:32:34 +00:00
// unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
2022-07-07 10:55:53 +00:00
const unqualified = c + String.fromCodePoint(65039);
2022-07-03 08:12:57 +00:00
if (c in unicodeMapping) {
if (open) { // unicode emoji inside colon
2022-07-06 03:47:59 +00:00
clearStack();
2022-07-03 08:12:57 +00:00
}
buf += convertUnicode(c);
2022-07-07 10:55:53 +00:00
} else if (unqualified in unicodeMapping) {
if (open) { // unicode emoji inside colon
clearStack();
}
2022-07-04 20:30:35 +00:00
2022-07-07 10:55:53 +00:00
buf += convertUnicode(unqualified);
2022-07-03 08:12:57 +00:00
} else if (c === ':') {
stack += ':';
2022-07-04 20:30:35 +00:00
// we see another : we convert it and clear the stack buffer
2022-07-03 08:12:57 +00:00
if (open) {
buf += convertEmoji(stack, customEmojis);
2022-07-03 08:12:57 +00:00
stack = '';
}
open = !open;
} else {
if (open) {
stack += c;
2022-07-04 20:30:35 +00:00
// if the stack is non-null and we see invalid chars it's a string not emoji
// so we push it to the return result and clear it
2022-07-03 08:12:57 +00:00
if (!validEmojiChar(c)) {
2022-07-06 03:47:59 +00:00
clearStack();
2022-07-03 08:12:57 +00:00
}
} else {
buf += c;
2022-07-03 08:12:57 +00:00
}
}
}
2022-07-04 20:30:35 +00:00
// never found a closing colon so it's just a raw string
if (open) {
buf += stack;
2022-07-04 20:30:35 +00:00
}
return buf;
2022-07-03 08:12:57 +00:00
};
2022-07-06 03:47:59 +00:00
export const parseHTML = (str: string): { text: boolean, data: string }[] => {
const tokens = [];
let buf = '';
let stack = '';
let open = false;
2022-07-04 20:30:35 +00:00
for (const c of str) {
if (c === '<') {
if (open) {
tokens.push({ text: true, data: stack });
stack = '<';
} else {
tokens.push({ text: true, data: buf });
stack = '<';
open = true;
}
} else if (c === '>') {
if (open) {
open = false;
tokens.push({ text: false, data: stack + '>' });
stack = '';
buf = '';
} else {
buf += '>';
}
2022-07-03 08:12:57 +00:00
} else {
if (open) {
stack += c;
} else {
buf += c;
}
}
}
2022-07-03 08:12:57 +00:00
if (open) {
tokens.push({ text: true, data: buf + stack });
} else if (buf !== '') {
tokens.push({ text: true, data: buf });
}
2022-07-03 08:12:57 +00:00
return tokens;
};
const emojify = (str: string, customEmojis = {}) => {
return parseHTML(str)
.map(({ text, data }) => {
if (!text) return data;
if (data.length === 0 || data === ' ') return data;
return emojifyText(data, customEmojis);
})
.join('');
2022-07-03 08:12:57 +00:00
};
export default emojify;
2022-07-04 20:30:35 +00:00
export const buildCustomEmojis = (customEmojis: any) => {
2022-07-05 03:11:46 +00:00
const emojis: EmojiMart<EmojiMartCustom>[] = [];
2022-07-03 08:12:57 +00:00
customEmojis.forEach((emoji: any) => {
const shortcode = emoji.get('shortcode');
2022-07-04 20:30:35 +00:00
const url = emoji.get('static_url');
2022-07-03 08:12:57 +00:00
const name = shortcode.replace(':', '');
emojis.push({
id: name,
name,
keywords: [name],
skins: [{ src: url }],
});
});
return emojis;
};