Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Detect emoji unicode on a page, replace it with images (supplied by GitHub, for now). Goes great in your ~/.js
/**
*
* Here's a thing that will look through all the text nodes of a document, and
* upon encountering an emoji codepoint, will replace it with an image.
* For now, those images are pulled from GitHub, which isn't very nice, so I
* need to find a more suitable host.
*
* Much of this code was gleaned from staring at the minified GitHub JS.
*
* Copyright (c) 2013 Mark Wunsch. Licensed under the MIT License.
* @markwunsch
*
*/
(function replaceEmojiWithImages(root) {
var REGIONAL_INDICATOR_A = parseInt("1f1e6", 16),
REGIONAL_INDICATOR_Z = parseInt("1f1ff", 16),
IMAGE_HOST = "assets.github.com",
IMAGE_PATH = "/images/icons/emoji/unicode/",
IMAGE_EXT = ".png";
// String.fromCodePoint is super helpful
if (!String.fromCodePoint) {
/*!
* ES6 Unicode Shims 0.1
* (c) 2012 Steven Levithan <http://slevithan.com/>
* MIT License
**/
String.fromCodePoint = function fromCodePoint () {
var chars = [], point, offset, units, i;
for (i = 0; i < arguments.length; ++i) {
point = arguments[i];
offset = point - 0x10000;
units = point > 0xFFFF ? [0xD800 + (offset >> 10), 0xDC00 + (offset & 0x3FF)] : [point];
chars.push(String.fromCharCode.apply(null, units));
}
return chars.join("");
}
}
/**
* Create a treewalker to walk an element and return an Array of Text Nodes.
* This function is (hopefully) smart enough to exclude unwanted text nodes
* like whitespace and script tags.
* https://gist.github.com/mwunsch/4693383
*/
function getLegitTextNodes(element) {
if (!document.createTreeWalker) return [];
var blacklist = ['SCRIPT', 'OPTION', 'TEXTAREA'],
textNodes = [],
walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
function excludeBlacklistedNodes(node) {
if (blacklist.indexOf(node.parentElement.nodeName.toUpperCase()) >= 0) return NodeFilter.FILTER_REJECT;
if (String.prototype.trim && !node.nodeValue.trim().length) return NodeFilter.FILTER_SKIP;
return NodeFilter.FILTER_ACCEPT;
},
false
);
while(walker.nextNode()) textNodes.push(walker.currentNode);
return textNodes;
}
/**
* Determine if this browser supports emoji.
*/
function doesSupportEmoji() {
var context, smiley;
if (!document.createElement('canvas').getContext) return;
context = document.createElement('canvas').getContext('2d');
if (typeof context.fillText != 'function') return;
smile = String.fromCodePoint(0x1F604); // :smile: String.fromCharCode(55357) + String.fromCharCode(56835)
context.textBaseline = "top";
context.font = "32px Arial";
context.fillText(smile, 0, 0);
return context.getImageData(16, 16, 1, 1).data[0] !== 0;
}
/**
* For a UTF-16 (JavaScript's preferred encoding...kinda) surrogate pair,
* return a Unicode codepoint.
*/
function surrogatePairToCodepoint(lead, trail) {
return (lead - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000;
}
/**
* Get an Image element for an emoji codepoint (in hex).
*/
function getImageForCodepoint(hex) {
var img = document.createElement('IMG');
img.style.width = "1.4em";
img.style.verticalAlign = "top";
img.src = "//" + IMAGE_HOST + IMAGE_PATH + hex + IMAGE_EXT;
return img;
}
/**
* Convert an HTML string into a DocumentFragment, for insertion into the dom.
*/
function fragmentForString(htmlString) {
var tmpDoc = document.createElement('DIV'),
fragment = document.createDocumentFragment(),
childNode;
tmpDoc.innerHTML = htmlString;
while(childNode = tmpDoc.firstChild) {
fragment.appendChild(childNode);
}
return fragment;
}
/**
* Iterate through a list of nodes, find emoji, replace with images.
*/
function emojiReplace(nodes) {
var PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/g;
nodes.forEach(function (node) {
var replacement,
value = node.nodeValue,
matches = value.match(PATTERN);
if (matches) {
replacement = value.replace(PATTERN, function (match, p1, p2) {
var codepoint = surrogatePairToCodepoint(p1.charCodeAt(0), p2.charCodeAt(0)),
img = getImageForCodepoint(codepoint.toString(16));
return img.outerHTML;
});
node.parentNode.replaceChild(fragmentForString(replacement), node);
}
});
}
// Call everything we've defined
if (!doesSupportEmoji()) {
emojiReplace(getLegitTextNodes(document.body));
}
}(this));
@Lordnibbler

This comment has been minimized.

Copy link

Lordnibbler commented Nov 19, 2013

I get a weird error on this: Uncaught NotSupportedError: The implementation did not support the requested type of object or operation. Line 53.

Are you still able to use this .js file?

@mwunsch

This comment has been minimized.

Copy link
Owner Author

mwunsch commented Feb 4, 2014

Yes I can!

@szimek

This comment has been minimized.

Copy link

szimek commented Sep 8, 2014

@mwunsch Hey, first of all thanks for this code. However, it doesn't always work - e.g. ⬆️ (http://www.fontspace.com/unicode/analyzer/?q=%E2%AC%86%EF%B8%8F) is not replaced. It looks like regex pattern in emojiReplace doesn't match this character. GitHub also recently renamed some of their emoji images (github/gemoji@6b9cdd6), so that might be another issue.

@arctickiwi

This comment has been minimized.

Copy link

arctickiwi commented Jul 13, 2017

The tree walker doesn't quite work in IE11. You need to replace parentElement with parentNode (line 56)

@brunolemos

This comment has been minimized.

Copy link

brunolemos commented Jan 31, 2019

Any tip on how to handle emojis that are a combination of two others or more? Example: 🤦‍♀

EDIT: Fixed by using .codePointAt() https://thekevinscott.com/emojis-in-javascript/

@alkhachatryan

This comment has been minimized.

Copy link

alkhachatryan commented Feb 4, 2019

Great job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.