Skip to content

Instantly share code, notes, and snippets.

@mwunsch
Last active August 13, 2023 21:44
Show Gist options
  • Save mwunsch/4710561 to your computer and use it in GitHub Desktop.
Save mwunsch/4710561 to your computer and use it in GitHub Desktop.
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
Copy link

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
Copy link
Author

mwunsch commented Feb 4, 2014

Yes I can!

@szimek
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
Copy link

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

@brunolemos
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
Copy link

Great job!

@CeoFred
Copy link

CeoFred commented Apr 15, 2020

Super helpful, i was wondering how slack was able to do what they did.. does this work in text areas??

@ethaniel
Copy link

Super helpful, i was wondering how slack was able to do what they did.. does this work in text areas??

obviously not, as this replaces emojis with <img> tags.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment