UNPKG

@converse/headless

Version:

Converse.js Headless build

361 lines (341 loc) 9.7 kB
import { ASCII_REPLACE_REGEX, CODEPOINTS_REGEX } from './regexes.js'; import { unescapeHTML } from '../../utils/html.js'; import converse from '../../shared/api/public.js'; import _converse from '../../shared/_converse.js'; import { SHORTNAME_RE } from './constants.js'; const { u } = converse.env; // Closured cache const emojis_by_attribute = {}; const ASCII_LIST = { '*\\0/*': '1f646', '*\\O/*': '1f646', '-___-': '1f611', ":'-)": '1f602', "':-)": '1f605', "':-D": '1f605', '>:-)': '1f606', "':-(": '1f613', '>:-(': '1f620', ":'-(": '1f622', 'O:-)': '1f607', '0:-3': '1f607', '0:-)': '1f607', '0;^)': '1f607', 'O;-)': '1f607', '0;-)': '1f607', 'O:-3': '1f607', '-__-': '1f611', ':-Þ': '1f61b', '</3': '1f494', ":')": '1f602', ':-D': '1f603', "':)": '1f605', "'=)": '1f605', "':D": '1f605', "'=D": '1f605', '>:)': '1f606', '>;)': '1f606', '>=)': '1f606', ';-)': '1f609', '*-)': '1f609', ';-]': '1f609', ';^)': '1f609', "':(": '1f613', "'=(": '1f613', ':-*': '1f618', ':^*': '1f618', '>:P': '1f61c', 'X-P': '1f61c', '>:[': '1f61e', ':-(': '1f61e', ':-[': '1f61e', '>:(': '1f620', ":'(": '1f622', ';-(': '1f622', '>.<': '1f623', '#-)': '1f635', '%-)': '1f635', 'X-)': '1f635', '\\0/': '1f646', '\\O/': '1f646', '0:3': '1f607', '0:)': '1f607', 'O:)': '1f607', 'O=)': '1f607', 'O:3': '1f607', 'B-)': '1f60e', '8-)': '1f60e', 'B-D': '1f60e', '8-D': '1f60e', '-_-': '1f611', '>:\\': '1f615', '>:/': '1f615', ':-/': '1f615', ':-.': '1f615', ':-P': '1f61b', ':Þ': '1f61b', ':-b': '1f61b', ':-O': '1f62e', 'O_O': '1f62e', '>:O': '1f62e', ':-X': '1f636', ':-#': '1f636', ':-)': '1f642', '(y)': '1f44d', '<3': '2764', ':D': '1f603', '=D': '1f603', ';)': '1f609', '*)': '1f609', ';]': '1f609', ';D': '1f609', ':*': '1f618', '=*': '1f618', ':(': '1f61e', ':[': '1f61e', '=(': '1f61e', ':@': '1f620', ';(': '1f622', 'D:': '1f628', ':$': '1f633', '=$': '1f633', '#)': '1f635', '%)': '1f635', 'X)': '1f635', 'B)': '1f60e', '8)': '1f60e', ':/': '1f615', ':\\': '1f615', '=/': '1f615', '=\\': '1f615', ':L': '1f615', '=L': '1f615', ':P': '1f61b', '=P': '1f61b', ':b': '1f61b', ':O': '1f62e', ':X': '1f636', ':#': '1f636', '=X': '1f636', '=#': '1f636', ':)': '1f642', '=]': '1f642', '=)': '1f642', ':]': '1f642', }; function toCodePoint(unicode_surrogates) { const r = []; let p = 0; let i = 0; while (i < unicode_surrogates.length) { const c = unicode_surrogates.charCodeAt(i++); if (p) { r.push((0x10000 + ((p - 0xd800) << 10) + (c - 0xdc00)).toString(16)); p = 0; } else if (0xd800 <= c && c <= 0xdbff) { p = c; } else { r.push(c.toString(16)); } } return r.join('-'); } /** * If it already looks like a shortname, use it directly. * Otherwise try to resolve it via codepoint. * * @param {string} emoji - A unicode emoji or shortname */ export function emojiToShortname(emoji) { if (emoji.match(SHORTNAME_RE)) { return emoji; } else { const by_cp = getEmojisByAttribute('cp'); const cp = u.emojis.emojiToCodepointKey(emoji); return by_cp[cp]?.sn ?? emoji; } } /** * Convert a unicode emoji string to its codepoint key as used in the emoji data * (e.g. '❤️' → '2764-fe0f', '👍' → '1f44d'). * @param {string} emoji * @returns {string} */ export function emojiToCodepointKey(emoji) { return [...emoji].map((c) => c.codePointAt(0).toString(16)).join('-'); } /** * @param {number|string} codepoint * @returns {string} */ function fromCodePoint(codepoint) { let code = typeof codepoint === 'string' ? parseInt(codepoint, 16) : codepoint; if (code < 0x10000) { return String.fromCharCode(code); } code -= 0x10000; return String.fromCharCode(0xd800 + (code >> 10), 0xdc00 + (code & 0x3ff)); } /** * Converts unicode code points and code pairs to their respective characters * @param {string} unicode */ function convert(unicode) { if (unicode.indexOf('-') > -1) { const parts = []; const s = unicode.split('-'); for (let i = 0; i < s.length; i++) { const part = parseInt(s[i], 16); if (part >= 0x10000 && part <= 0x10ffff) { const hi = Math.floor((part - 0x10000) / 0x400) + 0xd800; const lo = ((part - 0x10000) % 0x400) + 0xdc00; parts.push(String.fromCharCode(hi) + String.fromCharCode(lo)); } else { parts.push(String.fromCharCode(part)); } } return parts.join(''); } return fromCodePoint(unicode); } /** * @param {string} str */ export function convertASCII2Emoji(str) { // Replace ASCII smileys return str.replace(ASCII_REPLACE_REGEX, (entire, _, m2, m3) => { if (typeof m3 === 'undefined' || m3 === '' || !(unescapeHTML(m3) in ASCII_LIST)) { // if the ascii doesn't exist just return the entire match return entire; } m3 = unescapeHTML(m3); const unicode = ASCII_LIST[m3].toUpperCase(); return m2 + convert(unicode); }); } /** * @param {string} text * @returns {import('./types').EmojiReference[]} */ export function getShortnameReferences(text) { if (!converse.emojis.initialized) { throw new Error( 'getShortnameReferences called before emojis are initialized. ' + 'To avoid this problem, first await the converse.emojis.initialized_promise', ); } const references = [...text.matchAll(converse.emojis.shortnames_regex)].filter((ref) => ref[0].length > 0); return references.map((ref) => { const cp = converse.emojis.by_sn[ref[0].toLowerCase()]?.cp; return { cp, begin: ref.index, end: ref.index + ref[0].length, shortname: ref[0], emoji: cp ? convert(cp) : null, }; }); } /** * @param {string} str * @param {Function} callback */ function parseStringForEmojis(str, callback) { const UFE0Fg = /\uFE0F/g; const U200D = String.fromCharCode(0x200d); return String(str).replace(CODEPOINTS_REGEX, (emoji, _, offset) => { const icon_id = toCodePoint(emoji.indexOf(U200D) < 0 ? emoji.replace(UFE0Fg, '') : emoji); if (icon_id) callback(icon_id, emoji, offset); return emoji; }); } /** * @param {string} text * @returns {import('./types').EmojiReference[]} */ export function getCodePointReferences(text) { const references = []; parseStringForEmojis(text, (icon_id, emoji, offset) => { references.push({ begin: offset, cp: icon_id, emoji: emoji, end: offset + emoji.length, shortname: getEmojisByAttribute('cp')[icon_id]?.sn || '', }); }); return references; } /** * @param {string} text * @returns {string[]} */ function addEmojisMarkup(text) { let list = [text]; [...getShortnameReferences(text), ...getCodePointReferences(text)] .sort((a, b) => b.begin - a.begin) .forEach((ref) => { const text = list.shift(); const emoji = ref.emoji || ref.shortname; list = [text.slice(0, ref.begin) + emoji + text.slice(ref.end), ...list]; }); return list; } /** * Replaces all shortnames in the passed in string with their * unicode (emoji) representation. * @param {String} str - String containing the shortname(s) * @returns {String} */ function shortnamesToUnicode(str) { return addEmojisMarkup(convertASCII2Emoji(str)).pop(); } /** * Determines whether the passed in string is just a single emoji shortname; * @namespace u * @method u.isOnlyEmojis * @param {String} text - A string which might be just an emoji shortname * @returns {Boolean} */ export function isOnlyEmojis(text) { const words = text.trim().split(/\s+/); if (words.length === 0 || words.length > 3) { return false; } const emojis = words.filter((text) => { const refs = getCodePointReferences(u.emojis.shortnamesToUnicode(text)); return refs.length === 1 && (text.toLowerCase() === refs[0]['shortname'] || text === refs[0]['emoji']); }); return emojis.length === words.length; } /** * @param { 'category'|'cp'|'sn' } attr * The attribute according to which the returned map should be keyed. * @returns { Object } * Map of emojis with the passed in `attr` used as key and a list of emojis as values. */ function getEmojisByAttribute(attr) { if (emojis_by_attribute[attr]) { return emojis_by_attribute[attr]; } if (attr === 'category') { return converse.emojis.json; } const all_variants = converse.emojis.list.map((e) => e[attr]).filter((c, i, arr) => arr.indexOf(c) == i); emojis_by_attribute[attr] = {}; all_variants.forEach((v) => (emojis_by_attribute[attr][v] = converse.emojis.list.find((i) => i[attr] === v))); return emojis_by_attribute[attr]; } Object.assign(u, { emojis: { convert, convertASCII2Emoji, emojiToCodepointKey, emojiToShortname, getCodePointReferences, getEmojisByAttribute, getShortnameReferences, isOnlyEmojis, shortnamesToUnicode, }, });