Skip to content

Instantly share code, notes, and snippets.

@timwhitlock
Last active December 16, 2015 08:09
Show Gist options
  • Select an option

  • Save timwhitlock/5403547 to your computer and use it in GitHub Desktop.

Select an option

Save timwhitlock/5403547 to your computer and use it in GitHub Desktop.
Code for Emojify bookmarklet at http://apps.timwhitlock.info/emoji/bookmark
/**
* Emojify all text nodes in a document
* @author Tim Whitlock
* @license MIT
*
* This is available as a compressed bookmarklet at:
* - http://apps.timwhitlock.info/emoji/tools/bookmarklet
*
* See the following links for algorithms and encodings:
* - https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/charCodeAt
*
*/
( function( window, document ){
// count how many emoji we find each time this is run.
//
window.emojifyCount = 0;
// ignore non-html pages
//
var body = document.body,
head = document.head || document.getElementsByTagName('head')[0];
if( ! body || ! head ){
return;
}
// logging only when console is available.
//
function log( text ){
window.console && console.log && console.log(text);
}
// simple span element creator
//
function createElement( className, innerText, tagName ){
var span = document.createElement( tagName||'span' );
span.className = className || '';
innerText && span.appendChild( createText(innerText) );
return span;
}
// simple text node creator
//
function createText( innerText ){
return document.createTextNode( innerText );
}
// convert surrogate pair to single integer
//
function compileSurrogate( hi, lo ){
return ( (hi - 0xD800) * 0x400 ) + (lo - 0xDC00) + 0x10000;
}
// convert emoji in a text node
//
function findEmoji( text ){
if( ! text ){
return [];
}
// single emoji range: [\u203C-\uFFFF]
// surrogate pair range: [\uD800-\uDBFF][\uDC00-\uDFFF]
var found = [],
reg = /[\u203C-\uFFFF][\uDC00-\uDFFF]?/g,
matches, match, lo, hi, index;
while( matches = reg.exec(text) ) {
match = matches[0];
index = reg.lastIndex;
hi = match.charCodeAt(0);
lo = match.charCodeAt(1);
// single emoji if no low sibling
if( isNaN(lo) ){
found.push( [ --index, 1, hi, match ] );
//log('single at '+index+' '+hi.toString(16) );
}
// else two single emojis if sibling is not in surrogate range
else if( hi < 0xD800 || hi > 0xDBFF ){
found.push( [ --index, 1, lo, match ] );
found.push( [ --index, 1, hi, match ] );
//log('two at '+index+' '+hi.toString(16)+' '+lo.toString(16) );
}
// else bingo - a surrogate pair we have
else {
found.push( [ index-=2, 2, compileSurrogate(hi,lo), match ] );
//log( 'surrogate at '+index+' '+compileSurrogate(hi,lo).toString(16) );
}
}
return found;
}
// convert emoji in a text node
//
function emojifyTextNode( textNode ){
var text = textNode.nodeValue,
found = findEmoji( text );
if( ! found.length ){
return;
}
// build new element stucture inside single span to keep child node count the same
var wrapNode = createElement('emojified'),
i = -1, next = 0, index, code, length;
while( ++i < found.length ){
index = found[i][0];
length = found[i][1];
code = found[i][2];
match = found[i][3];
// content before emoji element if we've skipped some text
if( index > next ){
wrapNode.appendChild( createText( text.substring( next, index ) ) );
}
// splice in emoji element
wrapNode.appendChild( createElement(
'emoji emoji-'+code.toString(16), match
) );
// ready for next
emojifyCount++;
next = index + length;
}
// pick up any remaining text
if( text.length > next ){
wrapNode.appendChild( createText( text.substr(next) ) );
}
textNode.parentNode.replaceChild( wrapNode, textNode );
}
// convert emoji in an element attribute
//
function emojifyAttribute( element, attr, text ){
text = text || element.getAttribute(attr);
var found = findEmoji( text );
if( ! found.length ){
return;
}
// can't do replacement in element attribute, can only set classname and add to count
var classes = element.className ? element.className.split(/\s+/) : [];
classes.push('emojified');
element.className = classes.join(' ');
emojifyCount += found.length;
}
// Recursively find every text node and its parent element
//
function descend( parent ){
// skip node if it's already converted in a previous run
if( -1 !== parent.className.indexOf('emojified') ){
return;
}
// skip node if it has no children
var length = parent.childNodes.length;
if( ! length ){
// support field input values
parent.hasAttribute('value') && emojifyAttribute( parent, 'value', parent.value );
return;
}
// skip tags we know will never contain emoji
switch( parent.tagName ){
case 'STYLE':
case 'SCRIPT':
return;
}
// ok to descend into this element
var i = -1, child;
while( ++i < length ){
child = parent.childNodes[i];
switch( child.nodeType ){
case 1:
descend( child );
break;
case 3:
emojifyTextNode( child );
break;
}
}
}
// find our own url amongst script sources
//
function myUrl(){
var i = -1, src,
baseurl = '/js/emoji/emojify/emojify.js',
scripts = document.documentElement.getElementsByTagName('script');
while( ++i < scripts.length ){
src = scripts[i].getAttribute('src');
if( src && src.indexOf(baseurl) !== -1 ){
return src;
}
}
return '/static'+baseurl;
}
// convert all text nodes
descend( body );
log('Emojified: '+emojifyCount);
// load CSS and Emoji fonts if needed and not already loaded
if( emojifyCount && ! window.emojiLoaded ){
var link = createElement('','','link');
link.setAttribute('href', myUrl().replace('/js/emoji/emojify/','/css/emoji/').replace('.js','.css') );
link.setAttribute('rel','stylesheet');
head.appendChild( link );
//
emojifyLoaded = true;
}
// done.
} )( window, document );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment