302 lines
7.9 KiB
JavaScript
302 lines
7.9 KiB
JavaScript
/*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
|
||
* @param {string} emoji
|
||
* @return {string}
|
||
*/
|
||
Emoji._get = function _get (emoji) {
|
||
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.<Object>}
|
||
*/
|
||
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;
|