diff --git a/lib/emoji.js b/lib/emoji.js index e7ad60b..9aeb20e 100644 --- a/lib/emoji.js +++ b/lib/emoji.js @@ -1,12 +1,13 @@ /*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 parser = /:([a-zA-Z0-9_\-\+]+):/g; +var emojiNameRegex = /:([a-zA-Z0-9_\-\+]+):/g; /** * Removes colons on either side @@ -14,18 +15,19 @@ var parser = /:([a-zA-Z0-9_\-\+]+):/g; * @param {string} str * @return {string} */ -var trim = function(str) { +var stripColons = function(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 trim(str); + return stripColons(str); } else { str = str.substr(colonIndex + 1); - return trim(str); + return stripColons(str); } } + return str; } @@ -36,16 +38,45 @@ var trim = function(str) { * @return {string} */ var wrapColons = function(str) { - return (str && str.length > 0) ? ':' + 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} + */ +var ensureColons = function(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. +var stripNSB = function(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 = module.exports = { - emoji: require('./emoji.json') +var Emoji = { + emoji: emojiByName, }; /** @@ -54,10 +85,11 @@ var Emoji = module.exports = { * @return {string} */ Emoji._get = function _get(emoji) { - if (Emoji.emoji.hasOwnProperty(emoji)) { - return Emoji.emoji[emoji]; + if (emojiByName.hasOwnProperty(emoji)) { + return emojiByName[emoji]; } - return wrapColons(emoji); + + return ensureColons(emoji); }; /** @@ -66,7 +98,7 @@ Emoji._get = function _get(emoji) { * @return {string} */ Emoji.get = function get(emoji) { - emoji = trim(emoji); + emoji = stripColons(emoji); return Emoji._get(emoji); }; @@ -78,20 +110,8 @@ Emoji.get = function get(emoji) { * @return {string} */ Emoji.which = function which(emoji_code, includeColons) { - var word = emojiToCode[emoji_code]; - if (word) { - return includeColons ? wrapColons(word) : word; - } - - // Most of the times, the word is already returned by now. Sometimes - // we need to handle the non-spacing-mark. If we haven't returned yet, - // we're going to try the oposite version, with or without the mark. - var endsWithMark = emoji_code[emoji_code.length - 1] === NON_SPACING_MARK; - var alias = endsWithMark - ? emoji_code.substr(0, emoji_code.length - 1) - : emoji_code + NON_SPACING_MARK; - - word = emojiToCode[alias]; + var code = stripNSB(emoji_code); + var word = emojiByCode[code]; return includeColons ? wrapColons(word) : word; }; @@ -106,7 +126,7 @@ Emoji.which = function which(emoji_code, includeColons) { Emoji.emojify = function emojify(str, on_missing, format) { if (!str) return ''; - return str.split(parser) // parse emoji via regex + 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; @@ -132,11 +152,11 @@ Emoji.emojify = function emojify(str, on_missing, format) { * @return {string} */ Emoji.random = function random() { - var emojiKeys = Object.keys(Emoji.emoji); + 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 { key: key, emoji: emoji }; } /** @@ -145,8 +165,8 @@ Emoji.random = function random() { * @return {Array.} */ Emoji.search = function search(str) { - var emojiKeys = Object.keys(Emoji.emoji); - var matcher = trim(str) + var emojiKeys = Object.keys(emojiByName); + var matcher = stripColons(str) var matchingKeys = emojiKeys.filter(function(key) { return key.toString().indexOf(matcher) === 0; }); @@ -158,10 +178,6 @@ Emoji.search = function search(str) { }); } -var emojiToCode = Object.keys(Emoji.emoji).reduce(function(h,k) { - return h[Emoji.emoji[k]] = k, h; -}, {}); - /** * unemojify a string (replace emoji with :emoji:) * @param {string} str @@ -175,3 +191,5 @@ Emoji.unemojify = function unemojify(str) { return Emoji.which(word, true) || word; }).join(''); }; + +module.exports = Emoji; diff --git a/package.json b/package.json index 8042288..32c5440 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "coverage": "./node_modules/.bin/istanbul cover _mocha test", "emojiparse": "node lib/emojiparse.js", "test": "./node_modules/.bin/mocha --require should --bail --reporter spec test/*", + "watch": "./node_modules/.bin/mocha --require should --bail --reporter spec test/* --watch", "prepublish": "npm run test" }, "main": "index.js",