1
0
mirror of https://github.com/twitter/twemoji.git synced 2024-06-15 03:35:16 +00:00
This commit is contained in:
Tom Wuttke 2016-12-12 01:48:21 +00:00 committed by GitHub
commit 1a547ae6b1
7 changed files with 356 additions and 478 deletions

View File

@ -71,8 +71,8 @@ If a callback is passed, the value of the `src` attribute will be the value retu
```js ```js
twemoji.parse( twemoji.parse(
'I \u2764\uFE0F emoji!', 'I \u2764\uFE0F emoji!',
function(icon, options, variant) { function(iconId, options) {
return '/assets/' + options.size + '/' + icon + '.gif'; return '/assets/' + options.size + '/' + iconId + '.gif';
} }
); );
@ -86,7 +86,7 @@ I <img
*/ */
``` ```
By default, the `options.size` parameter will be the string `"36x36"` and the `variant` will be an optional `\uFE0F` char that is usually ignored by default. If your assets include or distinguish between `\u2764\uFE0F` and `\u2764`, you might want to use such a variable. By default, the `options.size` parameter will be the string `"36x36"`.
_string parsing + callback returning_ `falsy` _string parsing + callback returning_ `falsy`
@ -95,11 +95,11 @@ If the callback returns "falsy values" such as `null`, `undefined`, `0`, `false`
var i = 0; var i = 0;
twemoji.parse( twemoji.parse(
'emoji, m\u2764\uFE0Fn am\u2764\uFE0Fur', 'emoji, m\u2764\uFE0Fn am\u2764\uFE0Fur',
function(icon, options, variant) { function(iconId, options) {
if (i++ === 0) { if (i++ === 0) {
return; // no changes made first call return; // no changes made first call
} }
return '/assets/' + icon + options.ext; return '/assets/' + iconId + options.ext;
} }
); );
@ -120,8 +120,8 @@ In case an object is passed as second parameter, the passed `options` object wil
twemoji.parse( twemoji.parse(
'I \u2764\uFE0F emoji!', 'I \u2764\uFE0F emoji!',
{ {
callback: function(icon, options) { callback: function(iconId, options) {
return '/assets/' + options.size + '/' + icon + '.gif'; return '/assets/' + options.size + '/' + iconId + '.gif';
}, },
size: 128 size: 128
} }
@ -185,12 +185,12 @@ The function to invoke in order to generate image `src`(s).
By default it is a function like the following one: By default it is a function like the following one:
```js ```js
function imageSourceGenerator(icon, options) { function imageSourceGenerator(iconId, options) {
return ''.concat( return ''.concat(
options.base, // by default Twitter Inc. CDN options.base, // by default Twitter Inc. CDN
options.size, // by default "36x36" string options.size, // by default "36x36" string
'/', '/',
icon, // the found emoji as code point iconId, // the found emoji as a string of hex code points
options.ext // by default ".png" options.ext // by default ".png"
); );
} }
@ -201,9 +201,9 @@ The function to invoke in order to generate additional, custom attributes for th
By default it is a function like the following one: By default it is a function like the following one:
```js ```js
function attributesCallback(icon, variant) { function attributesCallback(rawText, iconId) {
return { return {
title: 'Emoji: ' + icon + variant title: 'Emoji: ' + rawText
}; };
} }
``` ```
@ -287,18 +287,18 @@ To properly support emoji, the document character set must be set to UTF-8. This
#### Exclude Characters (V1) #### Exclude Characters (V1)
To exclude certain characters from being replaced by twemoji.js, call twemoji.parse() with a callback, returning false for the specific unicode icon. For example: To exclude certain characters from being replaced by twemoji.js, call twemoji.parse() with a callback, returning false for the specific unicode iconId. For example:
```js ```js
twemoji.parse(document.body, { twemoji.parse(document.body, {
callback: function(icon, options, variant) { callback: function(iconId, options) {
switch ( icon ) { switch ( iconId ) {
case 'a9': // © copyright case 'a9': // © copyright
case 'ae': // ® registered trademark case 'ae': // ® registered trademark
case '2122': // ™ trademark case '2122': // ™ trademark
return false; return false;
} }
return ''.concat(options.base, options.size, '/', icon, options.ext); return ''.concat(options.base, options.size, '/', iconId, options.ext);
} }
}); });
``` ```

96
test.js
View File

@ -44,17 +44,17 @@ wru.test([{
name: 'string parsing + callback', name: 'string parsing + callback',
test: function () { test: function () {
var result = false; var result = false;
twemoji.parse('I \u2764 emoji!', function (icon, options, variant) { twemoji.parse('I \u2764 emoji!', function (icon, options) {
result = icon === '2764' && options.size === '36x36' && !variant; result = icon === '2764' && options.size === '36x36';
}); });
wru.assert('works OK without variant', result); wru.assert('works OK without variant', result);
result = false; result = false;
twemoji.parse('I \u2764\uFE0F emoji!', function (icon, options, variant) { twemoji.parse('I \u2764\uFE0F emoji!', function (icon, options) {
result = icon === '2764' && options.size === '36x36' && variant === '\uFE0F'; result = icon === '2764' && options.size === '36x36';
}); });
wru.assert('works OK with variant', result); wru.assert('works OK with variant', result);
result = true; result = true;
twemoji.parse('I \u2764\uFE0E emoji!', function (icon, options, variant) { twemoji.parse('I \u2764\uFE0E emoji!', function (icon, options) {
result = false; result = false;
}); });
wru.assert('not invoked when \uFE0E is matched', result); wru.assert('not invoked when \uFE0E is matched', result);
@ -83,26 +83,13 @@ wru.test([{
) )
); );
} }
},{
name: 'twemoji.replace(str, callback)',
test: function () {
var result = false;
var str = twemoji.replace('I \u2764\uFE0E emoji!', function (match, emoji, variant) {
result = match === '\u2764\uFE0E' &&
emoji === '\u2764\uFE0E' &&
variant === '\uFE0E';
return '<3';
});
wru.assert('all exepected values are passed through', result);
wru.assert('returned value is the expected', str === 'I <3 emoji!');
}
},{ },{
name: 'twemoji.test(str)', name: 'twemoji.test(str)',
test: function () { test: function () {
wru.assert( wru.assert(
twemoji.test('I \u2764 emoji!') && twemoji.test('I \u2764 emoji!') &&
twemoji.test('I \u2764\uFE0F emoji!') && twemoji.test('I \u2764\uFE0F emoji!') &&
twemoji.test('I \u2764\uFE0E emoji!') && !twemoji.test('I \u2764\uFE0E emoji!') &&
!twemoji.test('nope') !twemoji.test('nope')
); );
} }
@ -174,15 +161,15 @@ wru.test([{
var result = false, var result = false,
div = document.createElement('div'); div = document.createElement('div');
div.appendChild(document.createTextNode('I \u2764 emoji!')); div.appendChild(document.createTextNode('I \u2764 emoji!'));
twemoji.parse(div, function (icon, options, variant) { twemoji.parse(div, function (icon, options) {
result = icon === '2764' && options.size === '36x36' && !variant; result = icon === '2764' && options.size === '36x36';
}); });
wru.assert('works OK without variant', result); wru.assert('works OK without variant', result);
result = false; result = false;
div = document.createElement('div'); div = document.createElement('div');
div.appendChild(document.createTextNode('I \u2764\uFE0F emoji!')); div.appendChild(document.createTextNode('I \u2764\uFE0F emoji!'));
twemoji.parse(div, function (icon, options, variant) { twemoji.parse(div, function (icon, options) {
result = icon === '2764' && options.size === '36x36' && variant === '\uFE0F'; result = icon === '2764' && options.size === '36x36';
}); });
wru.assert('works OK with variant', result); wru.assert('works OK with variant', result);
result = true; result = true;
@ -209,8 +196,8 @@ wru.test([{
div.appendChild(document.createTextNode('I \u2764 emoji!')); div.appendChild(document.createTextNode('I \u2764 emoji!'));
twemoji.parse(div, { twemoji.parse(div, {
size: 16, size: 16,
callback: function (icon, options, variant) { callback: function (icon, options) {
result = icon === '2764' && options.size === '16x16' && !variant; result = icon === '2764' && options.size === '16x16';
} }
}); });
wru.assert('works OK without variant', result); wru.assert('works OK without variant', result);
@ -219,8 +206,8 @@ wru.test([{
div.appendChild(document.createTextNode('I \u2764\uFE0F emoji!')); div.appendChild(document.createTextNode('I \u2764\uFE0F emoji!'));
twemoji.parse(div, { twemoji.parse(div, {
size: 72, size: 72,
callback: function (icon, options, variant) { callback: function (icon, options) {
result = icon === '2764' && options.size === '72x72' && !!variant; result = icon === '2764' && options.size === '72x72';
} }
}); });
wru.assert('works OK with variant', result); wru.assert('works OK with variant', result);
@ -315,9 +302,9 @@ wru.test([{
twemoji.parse( twemoji.parse(
'I \u2764 emoji!', 'I \u2764 emoji!',
{ {
attributes: function(icon) { attributes: function(rawText, iconId) {
return { return {
title: 'Emoji: ' + icon, title: 'Emoji: ' + rawText,
'data-test': 'We all <3 emoji' 'data-test': 'We all <3 emoji'
}; };
} }
@ -326,7 +313,25 @@ wru.test([{
); );
} }
},{ },{
name: 'string parsing + attributes callback content properly encoded', name: 'string parsing + attributes callback icon id',
test: function () {
wru.assert(
'custom attributes are inserted',
'I <img class="emoji" draggable="false" alt="\u2764" src="' + base + '36x36/2764.png" title="Emoji: 2764" data-test="We all &lt;3 emoji"> emoji!' ===
twemoji.parse(
'I \u2764 emoji!',
{
attributes: function(rawText, iconId) {
return {
title: 'Emoji: ' + iconId,
'data-test': 'We all <3 emoji'
};
}
}
)
);
}
},{ name: 'string parsing + attributes callback content properly encoded',
test: function () { test: function () {
wru.assert( wru.assert(
'custom attributes are inserted', 'custom attributes are inserted',
@ -334,7 +339,7 @@ wru.test([{
twemoji.parse( twemoji.parse(
'I \u2764 emoji!', 'I \u2764 emoji!',
{ {
attributes: function(icon) { attributes: function(rawText, iconId) {
return { return {
title: '&amp;lt;script&amp;gt;alert("yo")&amp;lt;/script&amp;gt;' title: '&amp;lt;script&amp;gt;alert("yo")&amp;lt;/script&amp;gt;'
}; };
@ -352,7 +357,7 @@ wru.test([{
twemoji.parse( twemoji.parse(
'I \u2764 emoji!', 'I \u2764 emoji!',
{ {
attributes: function(icon) { attributes: function(rawText, iconId) {
return { return {
title: 'test', title: 'test',
onsomething: 'whoops!', onsomething: 'whoops!',
@ -373,9 +378,9 @@ wru.test([{
div.appendChild(document.createTextNode('I \u2764 emoji!')); div.appendChild(document.createTextNode('I \u2764 emoji!'));
twemoji.parse( twemoji.parse(
div, { div, {
attributes: function(icon) { attributes: function(rawText, iconId) {
return { return {
title: 'Emoji: ' + icon, title: 'Emoji: ' + rawText,
'data-test': 'We all <3 emoji', 'data-test': 'We all <3 emoji',
onclick: 'nope', onclick: 'nope',
onmousedown: 'nada' onmousedown: 'nada'
@ -462,36 +467,17 @@ wru.test([{
wru.assert('the length is preserved', wru.assert('the length is preserved',
div.getElementsByTagName('img')[0].alt.length === 2); div.getElementsByTagName('img')[0].alt.length === 2);
} }
},{ }, {
name: 'multiple parsing using a callback', name: 'multiple parsing using a callback',
test: function () { test: function () {
wru.assert( wru.assert(
'FE0E is still ignored', 'FE0E is still ignored',
twemoji.parse('\u25c0 \u25c0\ufe0e \u25c0\ufe0f', { twemoji.parse('\u25c0 \u25c0\ufe0e \u25c0\ufe0f', {
callback: function(icon){ return 'icon'; } callback: function(iconId, options){return 'icon';}
}) === }) ===
'<img class="emoji" draggable="false" alt="\u25c0" src="icon"> \u25c0\ufe0e <img class="emoji" draggable="false" alt="\u25c0\ufe0f" src="icon">' '<img class="emoji" draggable="false" alt="\u25c0" src="icon"> \u25c0\ufe0e <img class="emoji" draggable="false" alt="\u25c0\ufe0f" src="icon">'
); );
} }
},{
name: 'non standard variant within others',
test: function () {
var a = [
'normal',
'forced-as-text',
'forced-as-apple-graphic',
'forced-as-graphic'
];
wru.assert('normal forced-as-text forced-as-apple-graphic forced-as-graphic' ===
twemoji.replace('\u25c0 \u25c0\ufe0e 5\ufe0f\u20e3 \u25c0\ufe0f', function(match, icon, variant){
if (variant === '\uFE0E') return a[1];
if (variant === '\uFE0F') return a[3];
if (!variant) return a[
icon.length === 3 && icon.charAt(1) === '\uFE0F' ? 2 : 0
];
})
);
}
},{ },{
name: 'invalid variants and chars', name: 'invalid variants and chars',
test: function () { test: function () {

View File

@ -229,11 +229,11 @@ Queue([
} }
}); });
// create a RegExp with properly ordered matches // create a RegExp
q.re = '((?:' + // the sensitive ones may be followed by U+FE0F but not U+FE0E
regular.join('|') + ')|(?:(?:' + q.re = regular.join('|') + '|(?:' +
sensitive.join('|') + sensitive.join('|') +
')([\\uFE0E\\uFE0F]?)))'; ')(?:\\ufe0f|(?!\\ufe0e))';
q.next(); q.next();
@ -413,16 +413,11 @@ function createTwemoji(re) {
* those follwed by the invariant \uFE0E ("as text"). * those follwed by the invariant \uFE0E ("as text").
* Once invoked, parameters will be: * Once invoked, parameters will be:
* *
* codePoint:string the lower case HEX code point * iconId:string the lower case HEX code point
* i.e. "1f4a9" * i.e. "1f4a9"
* *
* options:Object all info for this parsing operation * options:Object all info for this parsing operation
* *
* variant:char the optional \uFE0F ("as image")
* variant, in case this info
* is anyhow meaningful.
* By default this is ignored.
*
* If such callback will return a falsy value instead * If such callback will return a falsy value instead
* of a valid `src` to use for the image, nothing will * of a valid `src` to use for the image, nothing will
* actually change for that specific emoji. * actually change for that specific emoji.
@ -441,16 +436,16 @@ function createTwemoji(re) {
* // I <img class="emoji" draggable="false" alt="❤️" src="/assets/2764.gif"> emoji! * // I <img class="emoji" draggable="false" alt="❤️" src="/assets/2764.gif"> emoji!
* *
* *
* twemoji.parse("I \u2764\uFE0F emoji!", function(icon, options, variant) { * twemoji.parse("I \u2764\uFE0F emoji!", function(iconId, options) {
* return '/assets/' + icon + '.gif'; * return '/assets/' + iconId + '.gif';
* }); * });
* // I <img class="emoji" draggable="false" alt="❤️" src="/assets/2764.gif"> emoji! * // I <img class="emoji" draggable="false" alt="❤️" src="/assets/2764.gif"> emoji!
* *
* *
* twemoji.parse("I \u2764\uFE0F emoji!", { * twemoji.parse("I \u2764\uFE0F emoji!", {
* size: 72, * size: 72,
* callback: function(icon, options, variant) { * callback: function(iconId, options) {
* return '/assets/' + options.size + '/' + icon + options.ext; * return '/assets/' + options.size + '/' + iconId + options.ext;
* } * }
* }); * });
* // I <img class="emoji" draggable="false" alt="❤️" src="/assets/72x72/2764.png"> emoji! * // I <img class="emoji" draggable="false" alt="❤️" src="/assets/72x72/2764.png"> emoji!
@ -471,17 +466,10 @@ function createTwemoji(re) {
* String.prototype.replace(str, callback) * String.prototype.replace(str, callback)
* arguments such: * arguments such:
* callback( * callback(
* match, // the emoji match * rawText, // the emoji match
* icon, // the emoji text (same as text)
* variant // either '\uFE0E' or '\uFE0F', if present
* ); * );
* *
* and others commonly received via replace. * and others commonly received via replace.
*
* NOTE: When the variant \uFE0E is found, remember this is an explicit intent
* from the user: the emoji should **not** be replaced with an image.
* In \uFE0F case one, it's the opposite, it should be graphic.
* This utility convetion is that only \uFE0E are not translated into images.
*/ */
replace: replace, replace: replace,
@ -553,7 +541,6 @@ function createTwemoji(re) {
* based on Twitter CDN * based on Twitter CDN
* @param string the emoji codepoint string * @param string the emoji codepoint string
* @param string the default size to use, i.e. "36x36" * @param string the default size to use, i.e. "36x36"
* @param string optional "\uFE0F" variant char, ignored by default
* @return string the image source to use * @return string the image source to use
*/ */
function defaultImageSrcGenerator(icon, options) { function defaultImageSrcGenerator(icon, options) {
@ -592,19 +579,15 @@ function createTwemoji(re) {
/** /**
* Used to both remove the possible variant * Used to both remove the possible variant
* and to convert utf16 into code points * and to convert utf16 into code points.
* @param string the emoji surrogate pair * If there is a zero-width-joiner, leave the variant in.
* @param string the optional variant char, if any * @param string the raw text of the emoji match
*/ */
function grabTheRightIcon(icon, variant) { function grabTheRightIcon(rawText) {
// if variant is present as \uFE0F
return toCodePoint( return toCodePoint(
variant === '\uFE0F' ? rawText.indexOf('\u200D') < 0 ?
// the icon should not contain it rawText.replace(/\uFE0F/g, '') :
icon.slice(0, -1) : rawText
// fix non standard OSX behavior
(icon.length === 3 && icon.charAt(1) === '\uFE0F' ?
icon.charAt(0) + icon.charAt(2) : icon)
); );
} }
@ -635,9 +618,8 @@ function createTwemoji(re) {
i, i,
index, index,
img, img,
alt, rawText,
icon, iconId,
variant,
src; src;
while (length--) { while (length--) {
modified = false; modified = false;
@ -652,39 +634,35 @@ function createTwemoji(re) {
createText(text.slice(i, index)) createText(text.slice(i, index))
); );
} }
alt = match[0]; rawText = match[0];
icon = match[1]; iconId = grabTheRightIcon(rawText);
variant = match[2]; i = index + rawText.length;
i = index + alt.length; src = options.callback(
if (variant !== '\uFE0E') { iconId,
src = options.callback( options
grabTheRightIcon(icon, variant), );
options, if (src) {
variant img = new Image();
); img.onerror = options.onerror;
if (src) { img.setAttribute('draggable', 'false');
img = new Image(); attrib = options.attributes(rawText, iconId);
img.onerror = options.onerror; for (attrname in attrib) {
img.setAttribute('draggable', 'false'); if (
attrib = options.attributes(icon, variant); attrib.hasOwnProperty(attrname) &&
for (attrname in attrib) { // don't allow any handlers to be set + don't allow overrides
if ( attrname.indexOf('on') !== 0 &&
attrib.hasOwnProperty(attrname) && !img.hasAttribute(attrname)
// don't allow any handlers to be set + don't allow overrides ) {
attrname.indexOf('on') !== 0 && img.setAttribute(attrname, attrib[attrname]);
!img.hasAttribute(attrname)
) {
img.setAttribute(attrname, attrib[attrname]);
}
} }
img.className = options.className;
img.alt = alt;
img.src = src;
modified = true;
fragment.appendChild(img);
} }
img.className = options.className;
img.alt = rawText;
img.src = src;
modified = true;
fragment.appendChild(img);
} }
if (!img) fragment.appendChild(createText(alt)); if (!img) fragment.appendChild(createText(rawText));
img = null; img = null;
} }
// is there actually anything to replace in here ? // is there actually anything to replace in here ?
@ -717,50 +695,45 @@ function createTwemoji(re) {
* @return the string with <img tags> replacing all found and parsed emoji * @return the string with <img tags> replacing all found and parsed emoji
*/ */
function parseString(str, options) { function parseString(str, options) {
return replace(str, function (match, icon, variant) { return replace(str, function (rawText) {
var var
ret = match, ret = rawText,
attrib, attrib,
attrname, attrname,
iconId,
src; src;
// verify the variant is not the FE0E one iconId = grabTheRightIcon(rawText);
// this variant means "emoji as text" and should not src = options.callback(
// require any action/replacement iconId,
// http://unicode.org/Public/UNIDATA/StandardizedVariants.html options
if (variant !== '\uFE0E') { );
src = options.callback( if (src) {
grabTheRightIcon(icon, variant), // recycle the match string replacing the emoji
options, // with its image counter part
variant ret = '<img '.concat(
'class="', options.className, '" ',
'draggable="false" ',
// needs to preserve user original intent
// when variants should be copied and pasted too
'alt="',
rawText,
'"',
' src="',
src,
'"'
); );
if (src) { attrib = options.attributes(rawText, iconId);
// recycle the match string replacing the emoji for (attrname in attrib) {
// with its image counter part if (
ret = '<img '.concat( attrib.hasOwnProperty(attrname) &&
'class="', options.className, '" ', // don't allow any handlers to be set + don't allow overrides
'draggable="false" ', attrname.indexOf('on') !== 0 &&
// needs to preserve user original intent ret.indexOf(' ' + attrname + '=') === -1
// when variants should be copied and pasted too ) {
'alt="', ret = ret.concat(' ', attrname, '="', escapeHTML(attrib[attrname]), '"');
match,
'"',
' src="',
src,
'"'
);
attrib = options.attributes(icon, variant);
for (attrname in attrib) {
if (
attrib.hasOwnProperty(attrname) &&
// don't allow any handlers to be set + don't allow overrides
attrname.indexOf('on') !== 0 &&
ret.indexOf(' ' + attrname + '=') === -1
) {
ret = ret.concat(' ', attrname, '="', escapeHTML(attrib[attrname]), '"');
}
} }
ret = ret.concat('>');
} }
ret = ret.concat('>');
} }
return ret; return ret;
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
twemoji.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long