Files
rehost-renderer/src/lib/emoji.ts
2025-10-17 17:02:59 -04:00

108 lines
2.8 KiB
TypeScript

import path from "path";
import { SKIP } from "unist-util-visit";
import { processMatches } from "./unified-processors";
import type { Plugin, Compiler } from "unified";
import { Element, Text, type Root } from "hast";
const EMOJI_REGEX = /:[a-zA-Z\d-_]+:/gims;
export type CustomEmoji = {
id: string;
name: string;
keywords: string[];
skins: { src: string }[];
native?: undefined;
// added by the library; we don't need to set it
shortcodes?: string;
};
type CustomEmojiCategory = {
id: string;
name: string;
emojis: CustomEmoji[];
};
export type CustomEmojiSet = CustomEmojiCategory[];
type NativeEmoji = {
id: string;
keywords: string[];
name: string;
native: string;
shortcodes: string;
unified: string;
};
export type Emoji = NativeEmoji | CustomEmoji;
export const customEmoji: CustomEmoji[] = [];
export const cohostPlusCustomEmoji: CustomEmoji[] = [];
export const indexableCustomEmoji = new Map<string, CustomEmoji>(
customEmoji.reduce<[string, CustomEmoji][]>((collector, emoji) => {
return [...collector, [emoji.name, emoji]];
}, [])
);
export const indexableCohostPlusCustomEmoji = new Map<string, CustomEmoji>(
cohostPlusCustomEmoji.reduce<[string, CustomEmoji][]>((collector, emoji) => {
return [...collector, [emoji.name, emoji]];
}, [])
);
type ParseOptions = {
cohostPlus: boolean;
};
export const parseEmoji = (options: ParseOptions) => {
const compiler: Compiler<Root> = processMatches(
EMOJI_REGEX,
(matches, splits, node, index, parent) => {
const els = splits.reduce<Array<Element | Text>>(
(collector, curr, index) => {
const currNode: Text = {
type: "text",
value: curr,
};
const pending = [...collector, currNode];
if (index < matches.length) {
const emojiName = matches[index].slice(
1,
matches[index].length - 1
);
let emoji = indexableCustomEmoji.get(emojiName);
if (!emoji && options.cohostPlus) {
emoji = indexableCohostPlusCustomEmoji.get(emojiName);
}
if (emoji) {
pending.push({
type: "element",
tagName: "CustomEmoji",
properties: {
name: emoji.name,
url: emoji.skins[0].src,
},
children: [],
} as Element);
} else {
pending.push({
type: "text",
value: matches[index],
});
}
}
return pending;
},
[] as Array<Element | Text>
);
parent.children.splice(index, 1, ...els);
// skip over all the new elements we just created
return [SKIP, index + els.length];
}
);
return compiler;
};