Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
148 lines
4.9 KiB
TypeScript
148 lines
4.9 KiB
TypeScript
import { asciiList } from '@rocket.chat/message-parser';
|
|
import { escapeHTML, escapeRegExp, unescapeHTML } from '@rocket.chat/tools';
|
|
|
|
import type { EmojiEntry } from './generateEmojiData';
|
|
import { getEmojiData } from './generateEmojiData';
|
|
import { legacyEmojioneMap } from './legacyEmojioneMap';
|
|
import { shortnameToUnicode } from './shortnameToUnicode';
|
|
import type { EmojiPackages } from '../../emoji/lib/rocketchat';
|
|
|
|
const emojiCategories = [
|
|
{ key: 'people', i18n: 'Smileys_and_People' },
|
|
{ key: 'nature', i18n: 'Animals_and_Nature' },
|
|
{ key: 'food', i18n: 'Food_and_Drink' },
|
|
{ key: 'activity', i18n: 'Activity' },
|
|
{ key: 'travel', i18n: 'Travel_and_Places' },
|
|
{ key: 'objects', i18n: 'Objects' },
|
|
{ key: 'symbols', i18n: 'Symbols' },
|
|
{ key: 'flags', i18n: 'Flags' },
|
|
];
|
|
|
|
// Build a reverse mapping from unicode emoji to shortcode for detection
|
|
let unicodeToShortcodeMap: Map<string, string> | null = null;
|
|
let emojiRegex: RegExp | null = null;
|
|
|
|
function getUnicodeToShortcodeMap(): Map<string, string> {
|
|
if (!unicodeToShortcodeMap) {
|
|
const { emojiList, bareAliases } = getEmojiData();
|
|
unicodeToShortcodeMap = new Map();
|
|
|
|
for (const [shortcode, entry] of Object.entries(emojiList)) {
|
|
const emojiEntry = entry;
|
|
if (emojiEntry.unicode && !unicodeToShortcodeMap.has(emojiEntry.unicode)) {
|
|
unicodeToShortcodeMap.set(emojiEntry.unicode, shortcode);
|
|
}
|
|
}
|
|
|
|
for (const [bare, shortcode] of bareAliases) {
|
|
if (!unicodeToShortcodeMap.has(bare)) {
|
|
unicodeToShortcodeMap.set(bare, shortcode);
|
|
}
|
|
}
|
|
}
|
|
return unicodeToShortcodeMap;
|
|
}
|
|
|
|
function getEmojiRegex(): RegExp {
|
|
if (!emojiRegex) {
|
|
const unicodeMap = getUnicodeToShortcodeMap();
|
|
// Sort emojis by length (longest first) to match multi-codepoint emojis correctly
|
|
const unicodeEmojis = [...unicodeMap.keys()].sort((a, b) => b.length - a.length);
|
|
const unicodePattern = unicodeEmojis.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
// Combine shortcode and unicode emoji patterns
|
|
emojiRegex = new RegExp(`(:([a-zA-Z0-9_+-]+):)|(${unicodePattern})`, 'g');
|
|
}
|
|
return emojiRegex;
|
|
}
|
|
|
|
// HTML-escaped variants are needed because the renderer receives escaped HTML (e.g. `>:(` arrives as `>:(`)
|
|
const asciiPattern = [...new Set(Object.keys(asciiList).flatMap((ascii) => [escapeHTML(ascii), ascii]))]
|
|
.sort((a, b) => b.length - a.length)
|
|
.map(escapeRegExp)
|
|
.join('|');
|
|
|
|
// `>` and `<` count as boundaries so emoticons touching tags still convert (e.g. `<p>hello :D</p>`)
|
|
const asciiRegex = new RegExp(
|
|
`<object[^>]*>.*?</object>|<span[^>]*>.*?</span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|(?<=^|\\s|>)(${asciiPattern})(?=\\s|$|[!,.?<])`,
|
|
'g',
|
|
);
|
|
|
|
function renderAsciiEmoji(text: string): string {
|
|
return text.replace(asciiRegex, (entire, ascii) => {
|
|
if (ascii === undefined) {
|
|
return entire;
|
|
}
|
|
|
|
const unescaped = unescapeHTML(ascii);
|
|
const unicode = asciiList[unescaped];
|
|
if (!unicode) {
|
|
return entire;
|
|
}
|
|
|
|
return `<span class="emoji" title="${escapeHTML(unescaped)}">${unicode}</span>`;
|
|
});
|
|
}
|
|
|
|
function renderEmoji(text: string, emojiPackages: EmojiPackages): string {
|
|
const { emojiList } = getEmojiData();
|
|
const unicodeMap = getUnicodeToShortcodeMap();
|
|
const pattern = getEmojiRegex();
|
|
|
|
const rendered = text.replace(pattern, (match, shortcodeGroup, shortcodeName, unicodeGroup) => {
|
|
// If it's a shortcode pattern (:emoji:)
|
|
if (shortcodeGroup) {
|
|
if (emojiPackages.list[`:${shortcodeName}:`]?.emojiPackage === 'emojiCustom') {
|
|
return match; // Don't render custom emojis as native
|
|
}
|
|
const key = `:${shortcodeName}:`;
|
|
const emoji = emojiList[key] as EmojiEntry | undefined;
|
|
if (emoji?.unicode) {
|
|
return `<span class="emoji" title="${shortcodeGroup}">${emoji.unicode}</span>`;
|
|
}
|
|
|
|
// Fallback to legacy emojione shortcodes for backward compatibility
|
|
const legacy = legacyEmojioneMap[shortcodeName];
|
|
if (legacy) {
|
|
return `<span class="emoji" title="${shortcodeGroup}">${legacy}</span>`;
|
|
}
|
|
|
|
return match;
|
|
}
|
|
|
|
// If it's a unicode emoji character (unicodeGroup will be defined)
|
|
if (unicodeGroup) {
|
|
const shortcode = unicodeMap.get(unicodeGroup);
|
|
if (shortcode) {
|
|
return `<span class="emoji" title="${shortcode}">${unicodeGroup}</span>`;
|
|
}
|
|
}
|
|
|
|
return match;
|
|
});
|
|
|
|
return emojiPackages.packages.native?.ascii ? renderAsciiEmoji(rendered) : rendered;
|
|
}
|
|
|
|
function renderPicker(emojiToRender: string): string | undefined {
|
|
const { emojiList } = getEmojiData();
|
|
const emoji = emojiList[emojiToRender] as EmojiEntry | undefined;
|
|
if (!emoji?.unicode) return undefined;
|
|
|
|
return `<span class="emoji" title="${emojiToRender}">${emoji.unicode}</span>`;
|
|
}
|
|
|
|
export const getEmojiConfig = (emojiPackages: EmojiPackages) => {
|
|
const { emojiList, emojisByCategory, toneList } = getEmojiData();
|
|
|
|
return {
|
|
emojiList,
|
|
emojisByCategory,
|
|
emojiCategories,
|
|
toneList,
|
|
render: (text: string) => renderEmoji(text, emojiPackages),
|
|
renderPicker,
|
|
sprites: false,
|
|
shortnameToUnicode,
|
|
};
|
|
};
|