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

170 wiersze
4.0 KiB
TypeScript
Czysty Zwykły widok Historia

2022-07-04 20:30:35 +00:00
// import data from '@emoji-mart/data';
2022-07-03 08:12:57 +00:00
import { load as cheerioLoad } from 'cheerio';
import { parseDocument } from 'htmlparser2';
2022-07-04 20:30:35 +00:00
import unicodeMapping from './mapping';
2022-07-03 08:12:57 +00:00
2022-07-04 20:30:35 +00:00
import type { Node as CheerioNode } from 'cheerio';
2022-07-05 02:08:47 +00:00
import type { Emoji as EmojiMart, CustomEmoji } from 'emoji-mart';
2022-07-03 08:12:57 +00:00
2022-07-04 20:30:35 +00:00
// export interface Emoji {
// id: string,
// custom: boolean,
// imageUrl: string,
// native: string,
// colons: string,
// }
2022-07-03 08:12:57 +00:00
2022-07-04 20:30:35 +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 = {}) => {
2022-07-03 08:12:57 +00:00
let res = '';
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
2022-07-04 20:30:35 +00:00
res += popStack(stack, open);
2022-07-03 08:12:57 +00:00
}
res += 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) {
2022-07-04 20:30:35 +00:00
res += 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-04 20:30:35 +00:00
res += popStack(stack, open);
2022-07-03 08:12:57 +00:00
}
} else {
res += c;
}
}
}
2022-07-04 20:30:35 +00:00
// never found a closing colon so it's just a raw string
if (open) {
res += stack;
}
2022-07-03 08:12:57 +00:00
return res;
};
2022-07-04 20:30:35 +00:00
// const parseHmtl = (str: string) => {
// const ret = [];
// let depth = 0;
//
// return ret;
// }
2022-07-03 08:12:57 +00:00
const filterTextNodes = (idx: number, el: CheerioNode) => {
return el.nodeType === Node.TEXT_NODE;
};
2022-07-04 20:30:35 +00:00
const emojify = (str: string, customEmojis = {}) => {
2022-07-03 08:12:57 +00:00
const dom = parseDocument(str);
const $ = cheerioLoad(dom, {
xmlMode: true,
decodeEntities: false,
});
$.root()
2022-07-04 20:30:35 +00:00
.contents() // iterate over flat map of all html elements
.filter(filterTextNodes) // only iterate over text nodes
2022-07-03 08:12:57 +00:00
.each((idx, el) => {
2022-07-03 08:45:30 +00:00
// skip common case
2022-07-03 08:12:57 +00:00
// @ts-ignore
2022-07-03 08:45:30 +00:00
if (el.data.length === 0 || el.data === ' ') return;
2022-07-03 08:12:57 +00:00
// mutating el.data is incorrect but we do it to prevent a second dom parse
// @ts-ignore
2022-07-04 20:30:35 +00:00
el.data = emojifyText(el.data, customEmojis);
2022-07-03 08:12:57 +00:00
});
return $.html();
};
export default emojify;
2022-07-04 20:30:35 +00:00
export const buildCustomEmojis = (customEmojis: any) => {
2022-07-05 02:08:47 +00:00
const emojis: EmojiMart<CustomEmoji>[] = [];
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;
};