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

217 wiersze
5.0 KiB
TypeScript
Czysty Zwykły widok Historia

2022-07-04 20:30:35 +00:00
import unicodeMapping from './mapping';
2022-07-03 08:12:57 +00:00
2022-07-05 03:11:46 +00:00
import type { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from 'emoji-mart';
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
*
* 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 {
2022-07-05 03:11:46 +00:00
id: string,
colons: string,
custom: true,
imageUrl: string,
}
2022-07-05 03:59:39 +00:00
export interface NativeEmoji {
id: string,
colons: string,
custom?: boolean,
2022-07-05 03:11:46 +00:00
unified: string,
native: string,
}
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
2022-07-05 03:11:46 +00:00
// export type Emoji = any;
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 === '_'
|| c === '-';
};
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];
return `<img draggable="false" class="emojione" alt="${c}" title=":${shortcode}:" src="/packs/emoji/${unified}.svg">`;
};
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
const popStack = (stack: string, open: boolean) => {
const ret = stack;
2022-07-03 08:12:57 +00:00
open = false;
stack = '';
2022-07-04 20:30:35 +00:00
return ret;
2022-07-03 08:12:57 +00:00
};
2022-07-04 20:30:35 +00:00
// TODO: handle grouped unicode emojis
export const emojifyText = (str: string, customEmojis = {}) => {
let buf = '';
2022-07-03 08:12:57 +00:00
let stack = '';
let open = false;
2022-07-04 20:30:35 +00:00
for (const c of Array.from(str)) { // chunk by unicode codepoint with Array.from
2022-07-03 08:12:57 +00:00
if (c in unicodeMapping) {
if (open) { // unicode emoji inside colon
buf += popStack(stack, open);
2022-07-03 08:12:57 +00:00
}
buf += convertUnicode(c);
2022-07-04 20:30:35 +00:00
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)) {
buf += popStack(stack, open);
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
};
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;
};