/*jslint node: true*/ var toArray = require('lodash/toArray'); var emojiByName = require('./emoji.json'); "use strict"; /** * regex to parse emoji in a string - finds emoji, e.g. :coffee: */ var emojiNameRegex = /:([a-zA-Z0-9_\-\+]+):/g; /** * regex to trim whitespace * use instead of String.prototype.trim() for IE8 support */ var trimSpaceRegex = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; /** * Removes colons on either side * of the string if present * @param {string} str * @return {string} */ function stripColons (str) { var colonIndex = str.indexOf(':'); if (colonIndex > -1) { // :emoji: (http://www.emoji-cheat-sheet.com/) if (colonIndex === str.length - 1) { str = str.substring(0, colonIndex); return stripColons(str); } else { str = str.substr(colonIndex + 1); return stripColons(str); } } return str; } /** * Adds colons to either side * of the string * @param {string} str * @return {string} */ function wrapColons (str) { return (typeof str === 'string' && str.length > 0) ? ':' + str + ':' : str; } /** * Ensure that the word is wrapped in colons * by only adding them, if they are not there. * @param {string} str * @return {string} */ function ensureColons (str) { return (typeof str === 'string' && str[0] !== ':') ? wrapColons(str) : str; } // Non spacing mark, some emoticons have them. It's the 'Variant Form', // which provides more information so that emoticons can be rendered as // more colorful graphics. FE0E is a unicode text version, where as FE0F // should be rendered as a graphical version. The code gracefully degrades. var NON_SPACING_MARK = String.fromCharCode(65039); // 65039 - '️' - 0xFE0F; var nonSpacingRegex = new RegExp(NON_SPACING_MARK, 'g') // Remove the non-spacing-mark from the code, never send a stripped version // to the client, as it kills graphical emoticons. function stripNSB (code) { return code.replace(nonSpacingRegex, ''); }; // Reversed hash table, where as emojiByName contains a { heart: '❤' } // dictionary emojiByCode contains { ❤: 'heart' }. The codes are normalized // to the text version. var emojiByCode = Object.keys(emojiByName).reduce(function(h,k) { h[stripNSB(emojiByName[k])] = k; return h; }, {}); /** * Emoji namespace */ var Emoji = { emoji: emojiByName, }; /** * get emoji code from name. return emoji code back if code is passed in. * @param {string} emoji * @return {string} */ Emoji._get = function _get (emoji) { if (emojiByCode[stripNSB(emoji)]) { return emoji; } else if (emojiByName.hasOwnProperty(emoji)) { return emojiByName[emoji]; } return ensureColons(emoji); }; /** * get emoji code from :emoji: string or name * @param {string} emoji * @return {string} */ Emoji.get = function get (emoji) { emoji = stripColons(emoji); return Emoji._get(emoji); }; /** * find the emoji by either code or name * @param {string} nameOrCode The emoji to find, either `coffee`, `:coffee:` or `☕`; * @return {object} */ Emoji.find = function find (nameOrCode) { return Emoji.findByName(nameOrCode) || Emoji.findByCode(nameOrCode); }; /** * find the emoji by name * @param {string} name The emoji to find either `coffee` or `:coffee:`; * @return {object} */ Emoji.findByName = function findByName (name) { var stripped = stripColons(name); var emoji = emojiByName[stripped]; return emoji ? ({ emoji: emoji, key: stripped }) : undefined; }; /** * find the emoji by code (emoji) * @param {string} code The emoji to find; for example `☕` or `☔` * @return {object} */ Emoji.findByCode = function findByCode (code) { var stripped = stripNSB(code); var name = emojiByCode[stripped]; // lookup emoji to ensure the Variant Form is returned return name ? ({ emoji: emojiByName[name], key: name }) : undefined; }; /** * Check if an emoji is known by this library * @param {string} nameOrCode The emoji to validate, either `coffee`, `:coffee:` or `☕`; * @return {object} */ Emoji.hasEmoji = function hasEmoji (nameOrCode) { return Emoji.hasEmojiByName(nameOrCode) || Emoji.hasEmojiByCode(nameOrCode); }; /** * Check if an emoji with given name is known by this library * @param {string} name The emoji to validate either `coffee` or `:coffee:`; * @return {object} */ Emoji.hasEmojiByName = function hasEmojiByName (name) { var result = Emoji.findByName(name); return !!result && result.key === stripColons(name); }; /** * Check if a given emoji is known by this library * @param {string} code The emoji to validate; for example `☕` or `☔` * @return {object} */ Emoji.hasEmojiByCode = function hasEmojiByCode (code) { var result = Emoji.findByCode(code); return !!result && stripNSB(result.emoji) === stripNSB(code); }; /** * get emoji name from code * @param {string} emoji * @param {boolean} includeColons should the result include the :: * @return {string} */ Emoji.which = function which (emoji_code, includeColons) { var code = stripNSB(emoji_code); var word = emojiByCode[code]; return includeColons ? wrapColons(word) : word; }; /** * emojify a string (replace :emoji: with an emoji) * @param {string} str * @param {function} on_missing (gets emoji name without :: and returns a proper emoji if no emoji was found) * @param {function} format (wrap the returned emoji in a custom element) * @return {string} */ Emoji.emojify = function emojify (str, on_missing, format) { if (!str) return ''; return str.split(emojiNameRegex) // parse emoji via regex .map(function parseEmoji(s, i) { // every second element is an emoji, e.g. "test :fast_forward:" -> [ "test ", "fast_forward" ] if (i % 2 === 0) return s; var emoji = Emoji._get(s); var isMissing = emoji.indexOf(':') > -1; if (isMissing && typeof on_missing === 'function') { return on_missing(s); } if (!isMissing && typeof format === 'function') { return format(emoji, s); } return emoji; }) .join('') // convert back to string ; }; /** * return a random emoji * @return {string} */ Emoji.random = function random () { var emojiKeys = Object.keys(emojiByName); var randomIndex = Math.floor(Math.random() * emojiKeys.length); var key = emojiKeys[randomIndex]; var emoji = Emoji._get(key); return { key: key, emoji: emoji }; } /** * return an collection of potential emoji matches * @param {string} str * @return {Array.} */ Emoji.search = function search (str) { var emojiKeys = Object.keys(emojiByName); var matcher = stripColons(str) var matchingKeys = emojiKeys.filter(function(key) { return key.toString().indexOf(matcher) === 0; }); return matchingKeys.map(function(key) { return { key: key, emoji: Emoji._get(key), }; }); } /** * unemojify a string (replace emoji with :emoji:) * @param {string} str * @return {string} */ Emoji.unemojify = function unemojify (str) { if (!str) return ''; var words = toArray(str); return words.map(function(word) { return Emoji.which(word, true) || word; }).join(''); }; /** * replace emojis with replacement value * @param {string} str * @param {function|string} the string or callback function to replace the emoji with * @param {boolean} should trailing whitespaces be cleaned? Defaults false * @return {string} */ Emoji.replace = function replace (str, replacement, cleanSpaces) { if (!str) return ''; var replace = typeof replacement === 'function' ? replacement : function() { return replacement; }; var words = toArray(str); var replaced = words.map(function(word, idx) { var emoji = Emoji.findByCode(word); if (emoji && cleanSpaces && words[idx + 1] === ' ') { words[idx + 1] = ''; } return emoji ? replace(emoji) : word; }).join(''); return cleanSpaces ? replaced.replace(trimSpaceRegex, '') : replaced; }; /** * remove all emojis from a string * @param {string} str * @return {string} */ Emoji.strip = function strip (str) { return Emoji.replace(str, '', true); }; module.exports = Emoji;