1
0
mirror of https://github.com/twitter/twemoji.git synced 2024-06-15 03:35:16 +00:00

Simplify variant handling

This commit is contained in:
Tom Wuttke 2016-02-25 15:29:12 -08:00
parent 685ed18255
commit 5e43152a57
6 changed files with 355 additions and 477 deletions

View File

@ -57,8 +57,8 @@ If a callback is passed, the `src` attribute will be the one returned by the sam
```js
twemoji.parse(
'I \u2764\uFE0F emoji!',
function(icon, options, variant) {
return '/assets/' + options.size + '/' + icon + '.gif';
function(iconId, options) {
return '/assets/' + options.size + '/' + iconId + '.gif';
}
);
@ -72,7 +72,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`
If the callback returns "falsy values" such `null`, `undefined`, `0`, `false`, or an empty string, nothing will change for that specific emoji.
@ -80,11 +80,11 @@ If the callback returns "falsy values" such `null`, `undefined`, `0`, `false`, o
var i = 0;
twemoji.parse(
'emoji, m\u2764\uFE0Fn am\u2764\uFE0Fur',
function(icon, options, variant) {
function(iconId, options) {
if (i++ === 0) {
return; // no changes made first call
}
return '/assets/' + icon + options.ext;
return '/assets/' + iconId + options.ext;
}
);
@ -104,8 +104,8 @@ In case an object is passed as second parameter, the passed `options` object wil
twemoji.parse(
'I \u2764\uFE0F emoji!',
{
callback: function(icon, options) {
return '/assets/' + options.size + '/' + icon + '.gif';
callback: function(iconId, options) {
return '/assets/' + options.size + '/' + iconId + '.gif';
},
size: 128
}
@ -169,12 +169,12 @@ The function to invoke in order to generate images `src`.
By default it is a function like the following one:
```js
function imageSourceGenerator(icon, options) {
function imageSourceGenerator(iconId, options) {
return ''.concat(
options.base, // by default Twitter Inc. CDN
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"
);
}
@ -185,9 +185,9 @@ The function to invoke in order to generate additional, custom attributes for th
By default it is a function like the following one:
```js
function attributesCallback(icon, variant) {
function attributesCallback(rawText, iconId) {
return {
title: 'Emoji: ' + icon + variant
title: 'Emoji: ' + rawText
};
}
```
@ -252,18 +252,18 @@ To properly support emoji, the document character must be set to UTF-8. This can
#### Exclude Characters
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
twemoji.parse(document.body, {
callback: function(icon, options, variant) {
switch ( icon ) {
callback: function(iconId, options) {
switch ( iconId ) {
case 'a9': // © copyright
case 'ae': // ® registered trademark
case '2122': // ™ trademark
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',
test: function () {
var result = false;
twemoji.parse('I \u2764 emoji!', function (icon, options, variant) {
result = icon === '2764' && options.size === '36x36' && !variant;
twemoji.parse('I \u2764 emoji!', function (icon, options) {
result = icon === '2764' && options.size === '36x36';
});
wru.assert('works OK without variant', result);
result = false;
twemoji.parse('I \u2764\uFE0F emoji!', function (icon, options, variant) {
result = icon === '2764' && options.size === '36x36' && variant === '\uFE0F';
twemoji.parse('I \u2764\uFE0F emoji!', function (icon, options) {
result = icon === '2764' && options.size === '36x36';
});
wru.assert('works OK with variant', result);
result = true;
twemoji.parse('I \u2764\uFE0E emoji!', function (icon, options, variant) {
twemoji.parse('I \u2764\uFE0E emoji!', function (icon, options) {
result = false;
});
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)',
test: function () {
wru.assert(
twemoji.test('I \u2764 emoji!') &&
twemoji.test('I \u2764\uFE0F emoji!') &&
twemoji.test('I \u2764\uFE0E emoji!') &&
!twemoji.test('I \u2764\uFE0E emoji!') &&
!twemoji.test('nope')
);
}
@ -174,15 +161,15 @@ wru.test([{
var result = false,
div = document.createElement('div');
div.appendChild(document.createTextNode('I \u2764 emoji!'));
twemoji.parse(div, function (icon, options, variant) {
result = icon === '2764' && options.size === '36x36' && !variant;
twemoji.parse(div, function (icon, options) {
result = icon === '2764' && options.size === '36x36';
});
wru.assert('works OK without variant', result);
result = false;
div = document.createElement('div');
div.appendChild(document.createTextNode('I \u2764\uFE0F emoji!'));
twemoji.parse(div, function (icon, options, variant) {
result = icon === '2764' && options.size === '36x36' && variant === '\uFE0F';
twemoji.parse(div, function (icon, options) {
result = icon === '2764' && options.size === '36x36';
});
wru.assert('works OK with variant', result);
result = true;
@ -209,8 +196,8 @@ wru.test([{
div.appendChild(document.createTextNode('I \u2764 emoji!'));
twemoji.parse(div, {
size: 16,
callback: function (icon, options, variant) {
result = icon === '2764' && options.size === '16x16' && !variant;
callback: function (icon, options) {
result = icon === '2764' && options.size === '16x16';
}
});
wru.assert('works OK without variant', result);
@ -219,8 +206,8 @@ wru.test([{
div.appendChild(document.createTextNode('I \u2764\uFE0F emoji!'));
twemoji.parse(div, {
size: 72,
callback: function (icon, options, variant) {
result = icon === '2764' && options.size === '72x72' && !!variant;
callback: function (icon, options) {
result = icon === '2764' && options.size === '72x72';
}
});
wru.assert('works OK with variant', result);
@ -315,9 +302,9 @@ wru.test([{
twemoji.parse(
'I \u2764 emoji!',
{
attributes: function(icon) {
attributes: function(rawText, iconId) {
return {
title: 'Emoji: ' + icon,
title: 'Emoji: ' + rawText,
'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 () {
wru.assert(
'custom attributes are inserted',
@ -334,7 +339,7 @@ wru.test([{
twemoji.parse(
'I \u2764 emoji!',
{
attributes: function(icon) {
attributes: function(rawText, iconId) {
return {
title: '&amp;lt;script&amp;gt;alert("yo")&amp;lt;/script&amp;gt;'
};
@ -352,7 +357,7 @@ wru.test([{
twemoji.parse(
'I \u2764 emoji!',
{
attributes: function(icon) {
attributes: function(rawText, iconId) {
return {
title: 'test',
onsomething: 'whoops!',
@ -373,9 +378,9 @@ wru.test([{
div.appendChild(document.createTextNode('I \u2764 emoji!'));
twemoji.parse(
div, {
attributes: function(icon) {
attributes: function(rawText, iconId) {
return {
title: 'Emoji: ' + icon,
title: 'Emoji: ' + rawText,
'data-test': 'We all <3 emoji',
onclick: 'nope',
onmousedown: 'nada'
@ -462,36 +467,17 @@ wru.test([{
wru.assert('the length is preserved',
div.getElementsByTagName('img')[0].alt.length === 2);
}
},{
}, {
name: 'multiple parsing using a callback',
test: function () {
wru.assert(
'FE0E is still ignored',
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">'
);
}
},{
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',
test: function () {

View File

@ -229,11 +229,11 @@ Queue([
}
});
// create a RegExp with properly ordered matches
q.re = '((?:' +
regular.join('|') + ')|(?:(?:' +
// create a RegExp
// the sensitive ones may be followed by U+FE0F but not U+FE0E
q.re = regular.join('|') + '|(?:' +
sensitive.join('|') +
')([\\uFE0E\\uFE0F]?)))';
')(?:\\ufe0f|(?!\\ufe0e))';
q.next();
@ -413,16 +413,11 @@ function createTwemoji(re) {
* those follwed by the invariant \uFE0E ("as text").
* Once invoked, parameters will be:
*
* codePoint:string the lower case HEX code point
* iconId:string the lower case HEX code point
* i.e. "1f4a9"
*
* 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
* of a valid `src` to use for the image, nothing will
* 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!
*
*
* twemoji.parse("I \u2764\uFE0F emoji!", function(icon, options, variant) {
* return '/assets/' + icon + '.gif';
* twemoji.parse("I \u2764\uFE0F emoji!", function(iconId, options) {
* return '/assets/' + iconId + '.gif';
* });
* // I <img class="emoji" draggable="false" alt="❤️" src="/assets/2764.gif"> emoji!
*
*
* twemoji.parse("I \u2764\uFE0F emoji!", {
* size: 72,
* callback: function(icon, options, variant) {
* return '/assets/' + options.size + '/' + icon + options.ext;
* callback: function(iconId, options) {
* return '/assets/' + options.size + '/' + iconId + options.ext;
* }
* });
* // 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)
* arguments such:
* callback(
* match, // the emoji match
* icon, // the emoji text (same as text)
* variant // either '\uFE0E' or '\uFE0F', if present
* rawText, // the emoji match
* );
*
* 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,
@ -553,7 +541,6 @@ function createTwemoji(re) {
* based on Twitter CDN
* @param string the emoji codepoint string
* @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
*/
function defaultImageSrcGenerator(icon, options) {
@ -592,19 +579,15 @@ function createTwemoji(re) {
/**
* Used to both remove the possible variant
* and to convert utf16 into code points
* @param string the emoji surrogate pair
* @param string the optional variant char, if any
* and to convert utf16 into code points.
* If there is a zero-width-joiner, leave the variant in.
* @param string the raw text of the emoji match
*/
function grabTheRightIcon(icon, variant) {
// if variant is present as \uFE0F
function grabTheRightIcon(rawText) {
return toCodePoint(
variant === '\uFE0F' ?
// the icon should not contain it
icon.slice(0, -1) :
// fix non standard OSX behavior
(icon.length === 3 && icon.charAt(1) === '\uFE0F' ?
icon.charAt(0) + icon.charAt(2) : icon)
rawText.indexOf('\u200D') < 0 ?
rawText.replace(/\uFE0F/g, '') :
rawText
);
}
@ -635,9 +618,8 @@ function createTwemoji(re) {
i,
index,
img,
alt,
icon,
variant,
rawText,
iconId,
src;
while (length--) {
modified = false;
@ -652,39 +634,35 @@ function createTwemoji(re) {
createText(text.slice(i, index))
);
}
alt = match[0];
icon = match[1];
variant = match[2];
i = index + alt.length;
if (variant !== '\uFE0E') {
src = options.callback(
grabTheRightIcon(icon, variant),
options,
variant
);
if (src) {
img = new Image();
img.onerror = options.onerror;
img.setAttribute('draggable', 'false');
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 &&
!img.hasAttribute(attrname)
) {
img.setAttribute(attrname, attrib[attrname]);
}
rawText = match[0];
iconId = grabTheRightIcon(rawText);
i = index + rawText.length;
src = options.callback(
iconId,
options
);
if (src) {
img = new Image();
img.onerror = options.onerror;
img.setAttribute('draggable', 'false');
attrib = options.attributes(rawText, iconId);
for (attrname in attrib) {
if (
attrib.hasOwnProperty(attrname) &&
// don't allow any handlers to be set + don't allow overrides
attrname.indexOf('on') !== 0 &&
!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;
}
// 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
*/
function parseString(str, options) {
return replace(str, function (match, icon, variant) {
return replace(str, function (rawText) {
var
ret = match,
ret = rawText,
attrib,
attrname,
iconId,
src;
// verify the variant is not the FE0E one
// this variant means "emoji as text" and should not
// require any action/replacement
// http://unicode.org/Public/UNIDATA/StandardizedVariants.html
if (variant !== '\uFE0E') {
src = options.callback(
grabTheRightIcon(icon, variant),
options,
variant
iconId = grabTheRightIcon(rawText);
src = options.callback(
iconId,
options
);
if (src) {
// recycle the match string replacing the emoji
// with its image counter part
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) {
// recycle the match string replacing the emoji
// with its image counter part
ret = '<img '.concat(
'class="', options.className, '" ',
'draggable="false" ',
// needs to preserve user original intent
// when variants should be copied and pasted too
'alt="',
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]), '"');
}
attrib = options.attributes(rawText, iconId);
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;
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long