diff --git a/1/templates/preview.html b/1/templates/preview.html new file mode 100644 index 00000000..1ee8325a --- /dev/null +++ b/1/templates/preview.html @@ -0,0 +1,55 @@ + + + + Twitter Emoji (Twemoji) Preview + + + + + + + + + diff --git a/1/test/preview-svg.html b/1/test/preview-svg.html new file mode 100644 index 00000000..cfb1657f --- /dev/null +++ b/1/test/preview-svg.html @@ -0,0 +1,928 @@ + + + + Twitter Emoji (Twemoji) Preview + + + + + + + + + diff --git a/1/test/preview.html b/1/test/preview.html new file mode 100644 index 00000000..db139f4d --- /dev/null +++ b/1/test/preview.html @@ -0,0 +1,928 @@ + + + + Twitter Emoji (Twemoji) Preview + + + + + + + + + diff --git a/1/twemoji.amd.js b/1/twemoji.amd.js new file mode 100644 index 00000000..44895644 --- /dev/null +++ b/1/twemoji.amd.js @@ -0,0 +1,569 @@ +define(function () { + /*jslint indent: 2, browser: true, bitwise: true, plusplus: true */ + var twemoji = (function ( + /*! Copyright Twitter Inc. and other contributors. Licensed under MIT *//* + https://github.com/twitter/twemoji/blob/gh-pages/LICENSE + */ + + // WARNING: this file is generated automatically via + // `node twemoji-generator.js` + // please update its `createTwemoji` function + // at the bottom of the same file instead. + + ) { + 'use strict'; + + /*jshint maxparams:4 */ + + var + // the exported module object + twemoji = { + + + ///////////////////////// + // properties // + ///////////////////////// + + // default assets url, by default will be Twitter Inc. CDN + base: 'https://twemoji.maxcdn.com/1/', + + // default assets file extensions, by default '.png' + ext: '.png', + + // default assets/folder size, by default "72x72" + // available via Twitter CDN: 72 + size: '72x72', + + // default class name, by default 'emoji' + className: 'emoji', + + // basic utilities / helpers to convert code points + // to JavaScript surrogates and vice versa + convert: { + + /** + * Given an HEX codepoint, returns UTF16 surrogate pairs. + * + * @param string generic codepoint, i.e. '1F4A9' + * @return string codepoint transformed into utf16 surrogates pair, + * i.e. \uD83D\uDCA9 + * + * @example + * twemoji.convert.fromCodePoint('1f1e8'); + * // "\ud83c\udde8" + * + * '1f1e8-1f1f3'.split('-').map(twemoji.convert.fromCodePoint).join('') + * // "\ud83c\udde8\ud83c\uddf3" + */ + fromCodePoint: fromCodePoint, + + /** + * Given UTF16 surrogate pairs, returns the equivalent HEX codepoint. + * + * @param string generic utf16 surrogates pair, i.e. \uD83D\uDCA9 + * @param string optional separator for double code points, default='-' + * @return string utf16 transformed into codepoint, i.e. '1F4A9' + * + * @example + * twemoji.convert.toCodePoint('\ud83c\udde8\ud83c\uddf3'); + * // "1f1e8-1f1f3" + * + * twemoji.convert.toCodePoint('\ud83c\udde8\ud83c\uddf3', '~'); + * // "1f1e8~1f1f3" + */ + toCodePoint: toCodePoint + }, + + + ///////////////////////// + // methods // + ///////////////////////// + + /** + * User first: used to remove missing images + * preserving the original text intent when + * a fallback for network problems is desired. + * Automatically added to Image nodes via DOM + * It could be recycled for string operations via: + * $('img.emoji').on('error', twemoji.onerror) + */ + onerror: function onerror() { + if (this.parentNode) { + this.parentNode.replaceChild(createText(this.alt), this); + } + }, + + /** + * Main method/logic to generate either tags or HTMLImage nodes. + * "emojify" a generic text or DOM Element. + * + * @overloads + * + * String replacement for `innerHTML` or server side operations + * twemoji.parse(string); + * twemoji.parse(string, Function); + * twemoji.parse(string, Object); + * + * HTMLElement tree parsing for safer operations over existing DOM + * twemoji.parse(HTMLElement); + * twemoji.parse(HTMLElement, Function); + * twemoji.parse(HTMLElement, Object); + * + * @param string|HTMLElement the source to parse and enrich with emoji. + * + * string replace emoji matches with tags. + * Mainly used to inject emoji via `innerHTML` + * It does **not** parse the string or validate it, + * it simply replaces found emoji with a tag. + * NOTE: be sure this won't affect security. + * + * HTMLElement walk through the DOM tree and find emoji + * that are inside **text node only** (nodeType === 3) + * Mainly used to put emoji in already generated DOM + * without compromising surrounding nodes and + * **avoiding** the usage of `innerHTML`. + * NOTE: Using DOM elements instead of strings should + * improve security without compromising too much + * performance compared with a less safe `innerHTML`. + * + * @param Function|Object [optional] + * either the callback that will be invoked or an object + * with all properties to use per each found emoji. + * + * Function if specified, this will be invoked per each emoji + * that has been found through the RegExp except + * those follwed by the invariant \uFE0E ("as text"). + * Once invoked, parameters will be: + * + * 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. + * + * + * Object if specified, an object containing the following properties + * + * callback Function the callback to invoke per each found emoji. + * base string the base url, by default twemoji.base + * ext string the image extension, by default twemoji.ext + * size string the assets size, by default twemoji.size + * + * @example + * + * twemoji.parse("I \u2764\uFE0F emoji!"); + * // I ❤️ emoji! + * + * + * twemoji.parse("I \u2764\uFE0F emoji!", function(iconId, options) { + * return '/assets/' + iconId + '.gif'; + * }); + * // I ❤️ emoji! + * + * + * twemoji.parse("I \u2764\uFE0F emoji!", { + * size: 72, + * callback: function(iconId, options) { + * return '/assets/' + options.size + '/' + iconId + options.ext; + * } + * }); + * // I ❤️ emoji! + * + */ + parse: parse, + + /** + * Given a string, invokes the callback argument + * per each emoji found in such string. + * This is the most raw version used by + * the .parse(string) method itself. + * + * @param string generic string to parse + * @param Function a generic callback that will be + * invoked to replace the content. + * This calback wil receive standard + * String.prototype.replace(str, callback) + * arguments such: + * callback( + * rawText, // the emoji match + * ); + * + * and others commonly received via replace. + */ + replace: replace, + + /** + * Simplify string tests against emoji. + * + * @param string some text that might contain emoji + * @return boolean true if any emoji was found, false otherwise. + * + * @example + * + * if (twemoji.test(someContent)) { + * console.log("emoji All The Things!"); + * } + */ + test: test + }, + + // used to escape HTML special chars in attributes + escaper = { + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }, + + // RegExp based on emoji's official Unicode standards + // http://www.unicode.org/Public/UNIDATA/EmojiSources.txt + re = /(?:[\u0039\u0038\u0037\u0036\u0035\u0034\u0033\u0032\u0031\u0030\u0023])\ufe0f?\u20e3|\ud83c\udde8\ud83c\uddf3|\ud83c\udde9\ud83c\uddea|\ud83c\uddea\ud83c\uddf8|\ud83c\uddeb\ud83c\uddf7|\ud83c\uddec\ud83c\udde7|\ud83c\uddee\ud83c\uddf9|\ud83c\uddef\ud83c\uddf5|\ud83c\uddf0\ud83c\uddf7|\ud83c\uddf7\ud83c\uddfa|\ud83c\uddfa\ud83c\uddf8|\ud83d[\udc00-\udc3e\udc40\udc42-\udcf7\udcf9-\udcfc\udd00-\udd3d\udd50-\udd67\uddfb-\ude42\ude45-\ude4f\ude80-\udec5]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf30-\udf35\udf37-\udf7c\udf80-\udf93\udfa0-\udfc4\udfc6-\udfca\udfe0-\udff0]|[\ue50a\u27bf\u27b0\u2797\u2796\u2795\u2755\u2754\u2753\u274e\u274c\u2728\u270b\u270a\u2705\u26ce\u23f3\u23f0\u23ec\u23eb\u23ea\u23e9]|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37]|[\u3299\u3297\u303d\u3030\u2b55\u2b50\u2b1c\u2b1b\u2b07\u2b06\u2b05\u2935\u2934\u27a1\u2764\u2757\u2747\u2744\u2734\u2733\u2716\u2714\u2712\u270f\u270c\u2709\u2708\u2702\u26fd\u26fa\u26f5\u26f3\u26f2\u26ea\u26d4\u26c5\u26c4\u26be\u26bd\u26ab\u26aa\u26a1\u26a0\u2693\u267f\u267b\u2668\u2666\u2665\u2663\u2660\u2653\u2652\u2651\u2650\u264f\u264e\u264d\u264c\u264b\u264a\u2649\u2648\u263a\u261d\u2615\u2614\u2611\u260e\u2601\u2600\u25fe\u25fd\u25fc\u25fb\u25c0\u25b6\u25ab\u25aa\u24c2\u231b\u231a\u21aa\u21a9\u2199\u2198\u2197\u2196\u2195\u2194\u2139\u2122\u2049\u203c\u00ae\u00a9])(?:\ufe0f|(?!\ufe0e))/g, + + // avoid runtime RegExp creation for not so smart, + // not JIT based, and old browsers / engines + UFE0Fg = /\uFE0F/g, + + // avoid using a string literal like '\u200D' here because minifiers expand it inline + U200D = String.fromCharCode(0x200D), + + // used to find HTML special chars in attributes + rescaper = /[&<>'"]/g, + + // nodes with type 1 which should **not** be parsed (including lower case svg) + shouldntBeParsed = /IFRAME|NOFRAMES|NOSCRIPT|SCRIPT|SELECT|STYLE|TEXTAREA|[a-z]/, + + // just a private shortcut + fromCharCode = String.fromCharCode; + + return twemoji; + + + ///////////////////////// + // private functions // + // declaration // + ///////////////////////// + + /** + * Shortcut to create text nodes + * @param string text used to create DOM text node + * @return Node a DOM node with that text + */ + function createText(text) { + return document.createTextNode(text); + } + + /** + * Utility function to escape html attribute text + * @param string text use in HTML attribute + * @return string text encoded to use in HTML attribute + */ + function escapeHTML(s) { + return s.replace(rescaper, replacer); + } + + /** + * Default callback used to generate emoji src + * based on Twitter CDN + * @param string the emoji codepoint string + * @param string the default size to use, i.e. "36x36" + * @return string the image source to use + */ + function defaultImageSrcGenerator(icon, options) { + return ''.concat(options.base, options.size, '/', icon, options.ext); + } + + /** + * Given a generic DOM nodeType 1, walk through all children + * and store every nodeType 3 (#text) found in the tree. + * @param Element a DOM Element with probably some text in it + * @param Array the list of previously discovered text nodes + * @return Array same list with new discovered nodes, if any + */ + function grabAllTextNodes(node, allText) { + var + childNodes = node.childNodes, + length = childNodes.length, + subnode, + nodeType; + while (length--) { + subnode = childNodes[length]; + nodeType = subnode.nodeType; + // parse emoji only in text nodes + if (nodeType === 3) { + // collect them to process emoji later + allText.push(subnode); + } + // ignore all nodes that are not type 1 or that + // should not be parsed as script, style, and others + else if (nodeType === 1 && !shouldntBeParsed.test(subnode.nodeName)) { + grabAllTextNodes(subnode, allText); + } + } + return allText; + } + + /** + * Used to both remove the possible variant + * and to convert utf16 into code points. + * If there is a zero-width-joiner (U+200D), leave the variants in. + * @param string the raw text of the emoji match + */ + function grabTheRightIcon(rawText) { + // if variant is present as \uFE0F + return toCodePoint(rawText.indexOf(U200D) < 0 ? + rawText.replace(UFE0Fg, '') : + rawText + ); + } + + /** + * DOM version of the same logic / parser: + * emojify all found sub-text nodes placing images node instead. + * @param Element generic DOM node with some text in some child node + * @param Object options containing info about how to parse + * + * .callback Function the callback to invoke per each found emoji. + * .base string the base url, by default twemoji.base + * .ext string the image extension, by default twemoji.ext + * .size string the assets size, by default twemoji.size + * + * @return Element same generic node with emoji in place, if any. + */ + function parseNode(node, options) { + var + allText = grabAllTextNodes(node, []), + length = allText.length, + attrib, + attrname, + modified, + fragment, + subnode, + text, + match, + i, + index, + img, + rawText, + iconId, + src; + while (length--) { + modified = false; + fragment = document.createDocumentFragment(); + subnode = allText[length]; + text = subnode.nodeValue; + i = 0; + while ((match = re.exec(text))) { + index = match.index; + if (index !== i) { + fragment.appendChild( + createText(text.slice(i, index)) + ); + } + 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 = rawText; + img.src = src; + modified = true; + fragment.appendChild(img); + } + if (!img) fragment.appendChild(createText(rawText)); + img = null; + } + // is there actually anything to replace in here ? + if (modified) { + // any text left to be added ? + if (i < text.length) { + fragment.appendChild( + createText(text.slice(i)) + ); + } + // replace the text node only, leave intact + // anything else surrounding such text + subnode.parentNode.replaceChild(fragment, subnode); + } + } + return node; + } + + /** + * String/HTML version of the same logic / parser: + * emojify a generic text placing images tags instead of surrogates pair. + * @param string generic string with possibly some emoji in it + * @param Object options containing info about how to parse + * + * .callback Function the callback to invoke per each found emoji. + * .base string the base url, by default twemoji.base + * .ext string the image extension, by default twemoji.ext + * .size string the assets size, by default twemoji.size + * + * @return the string with replacing all found and parsed emoji + */ + function parseString(str, options) { + return replace(str, function (rawText) { + var + ret = rawText, + iconId = grabTheRightIcon(rawText), + src = options.callback(iconId, options), + attrib, + attrname; + if (src) { + // recycle the match string replacing the emoji + // with its image counter part + ret = ''); + } + return ret; + }); + } + + /** + * Function used to actually replace HTML special chars + * @param string HTML special char + * @return string encoded HTML special char + */ + function replacer(m) { + return escaper[m]; + } + + /** + * Default options.attribute callback + * @return null + */ + function returnNull() { + return null; + } + + /** + * Given a generic value, creates its squared counterpart if it's a number. + * As example, number 36 will return '36x36'. + * @param any a generic value. + * @return any a string representing asset size, i.e. "36x36" + * only in case the value was a number. + * Returns initial value otherwise. + */ + function toSizeSquaredAsset(value) { + return typeof value === 'number' ? + value + 'x' + value : + value; + } + + + ///////////////////////// + // exported functions // + // declaration // + ///////////////////////// + + function fromCodePoint(codepoint) { + var code = typeof codepoint === 'string' ? + parseInt(codepoint, 16) : codepoint; + if (code < 0x10000) { + return fromCharCode(code); + } + code -= 0x10000; + return fromCharCode( + 0xD800 + (code >> 10), + 0xDC00 + (code & 0x3FF) + ); + } + + function parse(what, how) { + if (!how || typeof how === 'function') { + how = {callback: how}; + } + // if first argument is string, inject html tags + // otherwise use the DOM tree and parse text nodes only + return (typeof what === 'string' ? parseString : parseNode)(what, { + callback: how.callback || defaultImageSrcGenerator, + attributes: typeof how.attributes === 'function' ? how.attributes : returnNull, + base: typeof how.base === 'string' ? how.base : twemoji.base, + ext: how.ext || twemoji.ext, + size: how.folder || toSizeSquaredAsset(how.size || twemoji.size), + className: how.className || twemoji.className, + onerror: how.onerror || twemoji.onerror + }); + } + + function replace(text, callback) { + return String(text).replace(re, callback); + } + + function test(text) { + // IE6 needs a reset before too + re.lastIndex = 0; + var result = re.test(text); + re.lastIndex = 0; + return result; + } + + function toCodePoint(unicodeSurrogates, sep) { + var + r = [], + c = 0, + p = 0, + i = 0; + while (i < unicodeSurrogates.length) { + c = unicodeSurrogates.charCodeAt(i++); + if (p) { + r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16)); + p = 0; + } else if (0xD800 <= c && c <= 0xDBFF) { + p = c; + } else { + r.push(c.toString(16)); + } + } + return r.join(sep || '-'); + } + + }()); + return twemoji; +}); \ No newline at end of file diff --git a/1/twemoji.js b/1/twemoji.js new file mode 100644 index 00000000..1a5518a6 --- /dev/null +++ b/1/twemoji.js @@ -0,0 +1,566 @@ +/*jslint indent: 2, browser: true, bitwise: true, plusplus: true */ +var twemoji = (function ( + /*! Copyright Twitter Inc. and other contributors. Licensed under MIT *//* + https://github.com/twitter/twemoji/blob/gh-pages/LICENSE + */ + + // WARNING: this file is generated automatically via + // `node twemoji-generator.js` + // please update its `createTwemoji` function + // at the bottom of the same file instead. + +) { + 'use strict'; + + /*jshint maxparams:4 */ + + var + // the exported module object + twemoji = { + + + ///////////////////////// + // properties // + ///////////////////////// + + // default assets url, by default will be Twitter Inc. CDN + base: 'https://twemoji.maxcdn.com/1/', + + // default assets file extensions, by default '.png' + ext: '.png', + + // default assets/folder size, by default "72x72" + // available via Twitter CDN: 72 + size: '72x72', + + // default class name, by default 'emoji' + className: 'emoji', + + // basic utilities / helpers to convert code points + // to JavaScript surrogates and vice versa + convert: { + + /** + * Given an HEX codepoint, returns UTF16 surrogate pairs. + * + * @param string generic codepoint, i.e. '1F4A9' + * @return string codepoint transformed into utf16 surrogates pair, + * i.e. \uD83D\uDCA9 + * + * @example + * twemoji.convert.fromCodePoint('1f1e8'); + * // "\ud83c\udde8" + * + * '1f1e8-1f1f3'.split('-').map(twemoji.convert.fromCodePoint).join('') + * // "\ud83c\udde8\ud83c\uddf3" + */ + fromCodePoint: fromCodePoint, + + /** + * Given UTF16 surrogate pairs, returns the equivalent HEX codepoint. + * + * @param string generic utf16 surrogates pair, i.e. \uD83D\uDCA9 + * @param string optional separator for double code points, default='-' + * @return string utf16 transformed into codepoint, i.e. '1F4A9' + * + * @example + * twemoji.convert.toCodePoint('\ud83c\udde8\ud83c\uddf3'); + * // "1f1e8-1f1f3" + * + * twemoji.convert.toCodePoint('\ud83c\udde8\ud83c\uddf3', '~'); + * // "1f1e8~1f1f3" + */ + toCodePoint: toCodePoint + }, + + + ///////////////////////// + // methods // + ///////////////////////// + + /** + * User first: used to remove missing images + * preserving the original text intent when + * a fallback for network problems is desired. + * Automatically added to Image nodes via DOM + * It could be recycled for string operations via: + * $('img.emoji').on('error', twemoji.onerror) + */ + onerror: function onerror() { + if (this.parentNode) { + this.parentNode.replaceChild(createText(this.alt), this); + } + }, + + /** + * Main method/logic to generate either tags or HTMLImage nodes. + * "emojify" a generic text or DOM Element. + * + * @overloads + * + * String replacement for `innerHTML` or server side operations + * twemoji.parse(string); + * twemoji.parse(string, Function); + * twemoji.parse(string, Object); + * + * HTMLElement tree parsing for safer operations over existing DOM + * twemoji.parse(HTMLElement); + * twemoji.parse(HTMLElement, Function); + * twemoji.parse(HTMLElement, Object); + * + * @param string|HTMLElement the source to parse and enrich with emoji. + * + * string replace emoji matches with tags. + * Mainly used to inject emoji via `innerHTML` + * It does **not** parse the string or validate it, + * it simply replaces found emoji with a tag. + * NOTE: be sure this won't affect security. + * + * HTMLElement walk through the DOM tree and find emoji + * that are inside **text node only** (nodeType === 3) + * Mainly used to put emoji in already generated DOM + * without compromising surrounding nodes and + * **avoiding** the usage of `innerHTML`. + * NOTE: Using DOM elements instead of strings should + * improve security without compromising too much + * performance compared with a less safe `innerHTML`. + * + * @param Function|Object [optional] + * either the callback that will be invoked or an object + * with all properties to use per each found emoji. + * + * Function if specified, this will be invoked per each emoji + * that has been found through the RegExp except + * those follwed by the invariant \uFE0E ("as text"). + * Once invoked, parameters will be: + * + * 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. + * + * + * Object if specified, an object containing the following properties + * + * callback Function the callback to invoke per each found emoji. + * base string the base url, by default twemoji.base + * ext string the image extension, by default twemoji.ext + * size string the assets size, by default twemoji.size + * + * @example + * + * twemoji.parse("I \u2764\uFE0F emoji!"); + * // I ❤️ emoji! + * + * + * twemoji.parse("I \u2764\uFE0F emoji!", function(iconId, options) { + * return '/assets/' + iconId + '.gif'; + * }); + * // I ❤️ emoji! + * + * + * twemoji.parse("I \u2764\uFE0F emoji!", { + * size: 72, + * callback: function(iconId, options) { + * return '/assets/' + options.size + '/' + iconId + options.ext; + * } + * }); + * // I ❤️ emoji! + * + */ + parse: parse, + + /** + * Given a string, invokes the callback argument + * per each emoji found in such string. + * This is the most raw version used by + * the .parse(string) method itself. + * + * @param string generic string to parse + * @param Function a generic callback that will be + * invoked to replace the content. + * This calback wil receive standard + * String.prototype.replace(str, callback) + * arguments such: + * callback( + * rawText, // the emoji match + * ); + * + * and others commonly received via replace. + */ + replace: replace, + + /** + * Simplify string tests against emoji. + * + * @param string some text that might contain emoji + * @return boolean true if any emoji was found, false otherwise. + * + * @example + * + * if (twemoji.test(someContent)) { + * console.log("emoji All The Things!"); + * } + */ + test: test + }, + + // used to escape HTML special chars in attributes + escaper = { + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }, + + // RegExp based on emoji's official Unicode standards + // http://www.unicode.org/Public/UNIDATA/EmojiSources.txt + re = /(?:[\u0039\u0038\u0037\u0036\u0035\u0034\u0033\u0032\u0031\u0030\u0023])\ufe0f?\u20e3|\ud83c\udde8\ud83c\uddf3|\ud83c\udde9\ud83c\uddea|\ud83c\uddea\ud83c\uddf8|\ud83c\uddeb\ud83c\uddf7|\ud83c\uddec\ud83c\udde7|\ud83c\uddee\ud83c\uddf9|\ud83c\uddef\ud83c\uddf5|\ud83c\uddf0\ud83c\uddf7|\ud83c\uddf7\ud83c\uddfa|\ud83c\uddfa\ud83c\uddf8|\ud83d[\udc00-\udc3e\udc40\udc42-\udcf7\udcf9-\udcfc\udd00-\udd3d\udd50-\udd67\uddfb-\ude42\ude45-\ude4f\ude80-\udec5]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf30-\udf35\udf37-\udf7c\udf80-\udf93\udfa0-\udfc4\udfc6-\udfca\udfe0-\udff0]|[\ue50a\u27bf\u27b0\u2797\u2796\u2795\u2755\u2754\u2753\u274e\u274c\u2728\u270b\u270a\u2705\u26ce\u23f3\u23f0\u23ec\u23eb\u23ea\u23e9]|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37]|[\u3299\u3297\u303d\u3030\u2b55\u2b50\u2b1c\u2b1b\u2b07\u2b06\u2b05\u2935\u2934\u27a1\u2764\u2757\u2747\u2744\u2734\u2733\u2716\u2714\u2712\u270f\u270c\u2709\u2708\u2702\u26fd\u26fa\u26f5\u26f3\u26f2\u26ea\u26d4\u26c5\u26c4\u26be\u26bd\u26ab\u26aa\u26a1\u26a0\u2693\u267f\u267b\u2668\u2666\u2665\u2663\u2660\u2653\u2652\u2651\u2650\u264f\u264e\u264d\u264c\u264b\u264a\u2649\u2648\u263a\u261d\u2615\u2614\u2611\u260e\u2601\u2600\u25fe\u25fd\u25fc\u25fb\u25c0\u25b6\u25ab\u25aa\u24c2\u231b\u231a\u21aa\u21a9\u2199\u2198\u2197\u2196\u2195\u2194\u2139\u2122\u2049\u203c\u00ae\u00a9])(?:\ufe0f|(?!\ufe0e))/g, + + // avoid runtime RegExp creation for not so smart, + // not JIT based, and old browsers / engines + UFE0Fg = /\uFE0F/g, + + // avoid using a string literal like '\u200D' here because minifiers expand it inline + U200D = String.fromCharCode(0x200D), + + // used to find HTML special chars in attributes + rescaper = /[&<>'"]/g, + + // nodes with type 1 which should **not** be parsed (including lower case svg) + shouldntBeParsed = /IFRAME|NOFRAMES|NOSCRIPT|SCRIPT|SELECT|STYLE|TEXTAREA|[a-z]/, + + // just a private shortcut + fromCharCode = String.fromCharCode; + + return twemoji; + + + ///////////////////////// + // private functions // + // declaration // + ///////////////////////// + + /** + * Shortcut to create text nodes + * @param string text used to create DOM text node + * @return Node a DOM node with that text + */ + function createText(text) { + return document.createTextNode(text); + } + + /** + * Utility function to escape html attribute text + * @param string text use in HTML attribute + * @return string text encoded to use in HTML attribute + */ + function escapeHTML(s) { + return s.replace(rescaper, replacer); + } + + /** + * Default callback used to generate emoji src + * based on Twitter CDN + * @param string the emoji codepoint string + * @param string the default size to use, i.e. "36x36" + * @return string the image source to use + */ + function defaultImageSrcGenerator(icon, options) { + return ''.concat(options.base, options.size, '/', icon, options.ext); + } + + /** + * Given a generic DOM nodeType 1, walk through all children + * and store every nodeType 3 (#text) found in the tree. + * @param Element a DOM Element with probably some text in it + * @param Array the list of previously discovered text nodes + * @return Array same list with new discovered nodes, if any + */ + function grabAllTextNodes(node, allText) { + var + childNodes = node.childNodes, + length = childNodes.length, + subnode, + nodeType; + while (length--) { + subnode = childNodes[length]; + nodeType = subnode.nodeType; + // parse emoji only in text nodes + if (nodeType === 3) { + // collect them to process emoji later + allText.push(subnode); + } + // ignore all nodes that are not type 1 or that + // should not be parsed as script, style, and others + else if (nodeType === 1 && !shouldntBeParsed.test(subnode.nodeName)) { + grabAllTextNodes(subnode, allText); + } + } + return allText; + } + + /** + * Used to both remove the possible variant + * and to convert utf16 into code points. + * If there is a zero-width-joiner (U+200D), leave the variants in. + * @param string the raw text of the emoji match + */ + function grabTheRightIcon(rawText) { + // if variant is present as \uFE0F + return toCodePoint(rawText.indexOf(U200D) < 0 ? + rawText.replace(UFE0Fg, '') : + rawText + ); + } + + /** + * DOM version of the same logic / parser: + * emojify all found sub-text nodes placing images node instead. + * @param Element generic DOM node with some text in some child node + * @param Object options containing info about how to parse + * + * .callback Function the callback to invoke per each found emoji. + * .base string the base url, by default twemoji.base + * .ext string the image extension, by default twemoji.ext + * .size string the assets size, by default twemoji.size + * + * @return Element same generic node with emoji in place, if any. + */ + function parseNode(node, options) { + var + allText = grabAllTextNodes(node, []), + length = allText.length, + attrib, + attrname, + modified, + fragment, + subnode, + text, + match, + i, + index, + img, + rawText, + iconId, + src; + while (length--) { + modified = false; + fragment = document.createDocumentFragment(); + subnode = allText[length]; + text = subnode.nodeValue; + i = 0; + while ((match = re.exec(text))) { + index = match.index; + if (index !== i) { + fragment.appendChild( + createText(text.slice(i, index)) + ); + } + 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 = rawText; + img.src = src; + modified = true; + fragment.appendChild(img); + } + if (!img) fragment.appendChild(createText(rawText)); + img = null; + } + // is there actually anything to replace in here ? + if (modified) { + // any text left to be added ? + if (i < text.length) { + fragment.appendChild( + createText(text.slice(i)) + ); + } + // replace the text node only, leave intact + // anything else surrounding such text + subnode.parentNode.replaceChild(fragment, subnode); + } + } + return node; + } + + /** + * String/HTML version of the same logic / parser: + * emojify a generic text placing images tags instead of surrogates pair. + * @param string generic string with possibly some emoji in it + * @param Object options containing info about how to parse + * + * .callback Function the callback to invoke per each found emoji. + * .base string the base url, by default twemoji.base + * .ext string the image extension, by default twemoji.ext + * .size string the assets size, by default twemoji.size + * + * @return the string with replacing all found and parsed emoji + */ + function parseString(str, options) { + return replace(str, function (rawText) { + var + ret = rawText, + iconId = grabTheRightIcon(rawText), + src = options.callback(iconId, options), + attrib, + attrname; + if (src) { + // recycle the match string replacing the emoji + // with its image counter part + ret = ''); + } + return ret; + }); + } + + /** + * Function used to actually replace HTML special chars + * @param string HTML special char + * @return string encoded HTML special char + */ + function replacer(m) { + return escaper[m]; + } + + /** + * Default options.attribute callback + * @return null + */ + function returnNull() { + return null; + } + + /** + * Given a generic value, creates its squared counterpart if it's a number. + * As example, number 36 will return '36x36'. + * @param any a generic value. + * @return any a string representing asset size, i.e. "36x36" + * only in case the value was a number. + * Returns initial value otherwise. + */ + function toSizeSquaredAsset(value) { + return typeof value === 'number' ? + value + 'x' + value : + value; + } + + + ///////////////////////// + // exported functions // + // declaration // + ///////////////////////// + + function fromCodePoint(codepoint) { + var code = typeof codepoint === 'string' ? + parseInt(codepoint, 16) : codepoint; + if (code < 0x10000) { + return fromCharCode(code); + } + code -= 0x10000; + return fromCharCode( + 0xD800 + (code >> 10), + 0xDC00 + (code & 0x3FF) + ); + } + + function parse(what, how) { + if (!how || typeof how === 'function') { + how = {callback: how}; + } + // if first argument is string, inject html tags + // otherwise use the DOM tree and parse text nodes only + return (typeof what === 'string' ? parseString : parseNode)(what, { + callback: how.callback || defaultImageSrcGenerator, + attributes: typeof how.attributes === 'function' ? how.attributes : returnNull, + base: typeof how.base === 'string' ? how.base : twemoji.base, + ext: how.ext || twemoji.ext, + size: how.folder || toSizeSquaredAsset(how.size || twemoji.size), + className: how.className || twemoji.className, + onerror: how.onerror || twemoji.onerror + }); + } + + function replace(text, callback) { + return String(text).replace(re, callback); + } + + function test(text) { + // IE6 needs a reset before too + re.lastIndex = 0; + var result = re.test(text); + re.lastIndex = 0; + return result; + } + + function toCodePoint(unicodeSurrogates, sep) { + var + r = [], + c = 0, + p = 0, + i = 0; + while (i < unicodeSurrogates.length) { + c = unicodeSurrogates.charCodeAt(i++); + if (p) { + r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16)); + p = 0; + } else if (0xD800 <= c && c <= 0xDBFF) { + p = c; + } else { + r.push(c.toString(16)); + } + } + return r.join(sep || '-'); + } + +}()); \ No newline at end of file diff --git a/1/twemoji.min.js b/1/twemoji.min.js new file mode 100644 index 00000000..8e06bc85 --- /dev/null +++ b/1/twemoji.min.js @@ -0,0 +1,2 @@ +/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */ +var twemoji=function(){"use strict";var twemoji={base:"https://twemoji.maxcdn.com/1/",ext:".png",size:"72x72",className:"emoji",convert:{fromCodePoint:fromCodePoint,toCodePoint:toCodePoint},onerror:function onerror(){if(this.parentNode){this.parentNode.replaceChild(createText(this.alt),this)}},parse:parse,replace:replace,test:test},escaper={"&":"&","<":"<",">":">","'":"'",'"':"""},re=/(?:[\u0039\u0038\u0037\u0036\u0035\u0034\u0033\u0032\u0031\u0030\u0023])\ufe0f?\u20e3|\ud83c\udde8\ud83c\uddf3|\ud83c\udde9\ud83c\uddea|\ud83c\uddea\ud83c\uddf8|\ud83c\uddeb\ud83c\uddf7|\ud83c\uddec\ud83c\udde7|\ud83c\uddee\ud83c\uddf9|\ud83c\uddef\ud83c\uddf5|\ud83c\uddf0\ud83c\uddf7|\ud83c\uddf7\ud83c\uddfa|\ud83c\uddfa\ud83c\uddf8|\ud83d[\udc00-\udc3e\udc40\udc42-\udcf7\udcf9-\udcfc\udd00-\udd3d\udd50-\udd67\uddfb-\ude42\ude45-\ude4f\ude80-\udec5]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf30-\udf35\udf37-\udf7c\udf80-\udf93\udfa0-\udfc4\udfc6-\udfca\udfe0-\udff0]|[\ue50a\u27bf\u27b0\u2797\u2796\u2795\u2755\u2754\u2753\u274e\u274c\u2728\u270b\u270a\u2705\u26ce\u23f3\u23f0\u23ec\u23eb\u23ea\u23e9]|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37]|[\u3299\u3297\u303d\u3030\u2b55\u2b50\u2b1c\u2b1b\u2b07\u2b06\u2b05\u2935\u2934\u27a1\u2764\u2757\u2747\u2744\u2734\u2733\u2716\u2714\u2712\u270f\u270c\u2709\u2708\u2702\u26fd\u26fa\u26f5\u26f3\u26f2\u26ea\u26d4\u26c5\u26c4\u26be\u26bd\u26ab\u26aa\u26a1\u26a0\u2693\u267f\u267b\u2668\u2666\u2665\u2663\u2660\u2653\u2652\u2651\u2650\u264f\u264e\u264d\u264c\u264b\u264a\u2649\u2648\u263a\u261d\u2615\u2614\u2611\u260e\u2601\u2600\u25fe\u25fd\u25fc\u25fb\u25c0\u25b6\u25ab\u25aa\u24c2\u231b\u231a\u21aa\u21a9\u2199\u2198\u2197\u2196\u2195\u2194\u2139\u2122\u2049\u203c\u00ae\u00a9])(?:\ufe0f|(?!\ufe0e))/g,UFE0Fg=/\uFE0F/g,U200D=String.fromCharCode(8205),rescaper=/[&<>'"]/g,shouldntBeParsed=/IFRAME|NOFRAMES|NOSCRIPT|SCRIPT|SELECT|STYLE|TEXTAREA|[a-z]/,fromCharCode=String.fromCharCode;return twemoji;function createText(text){return document.createTextNode(text)}function escapeHTML(s){return s.replace(rescaper,replacer)}function defaultImageSrcGenerator(icon,options){return"".concat(options.base,options.size,"/",icon,options.ext)}function grabAllTextNodes(node,allText){var childNodes=node.childNodes,length=childNodes.length,subnode,nodeType;while(length--){subnode=childNodes[length];nodeType=subnode.nodeType;if(nodeType===3){allText.push(subnode)}else if(nodeType===1&&!shouldntBeParsed.test(subnode.nodeName)){grabAllTextNodes(subnode,allText)}}return allText}function grabTheRightIcon(rawText){return toCodePoint(rawText.indexOf(U200D)<0?rawText.replace(UFE0Fg,""):rawText)}function parseNode(node,options){var allText=grabAllTextNodes(node,[]),length=allText.length,attrib,attrname,modified,fragment,subnode,text,match,i,index,img,rawText,iconId,src;while(length--){modified=false;fragment=document.createDocumentFragment();subnode=allText[length];text=subnode.nodeValue;i=0;while(match=re.exec(text)){index=match.index;if(index!==i){fragment.appendChild(createText(text.slice(i,index)))}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)&&attrname.indexOf("on")!==0&&!img.hasAttribute(attrname)){img.setAttribute(attrname,attrib[attrname])}}img.className=options.className;img.alt=rawText;img.src=src;modified=true;fragment.appendChild(img)}if(!img)fragment.appendChild(createText(rawText));img=null}if(modified){if(i")}return ret})}function replacer(m){return escaper[m]}function returnNull(){return null}function toSizeSquaredAsset(value){return typeof value==="number"?value+"x"+value:value}function fromCodePoint(codepoint){var code=typeof codepoint==="string"?parseInt(codepoint,16):codepoint;if(code<65536){return fromCharCode(code)}code-=65536;return fromCharCode(55296+(code>>10),56320+(code&1023))}function parse(what,how){if(!how||typeof how==="function"){how={callback:how}}return(typeof what==="string"?parseString:parseNode)(what,{callback:how.callback||defaultImageSrcGenerator,attributes:typeof how.attributes==="function"?how.attributes:returnNull,base:typeof how.base==="string"?how.base:twemoji.base,ext:how.ext||twemoji.ext,size:how.folder||toSizeSquaredAsset(how.size||twemoji.size),className:how.className||twemoji.className,onerror:how.onerror||twemoji.onerror})}function replace(text,callback){return String(text).replace(re,callback)}function test(text){re.lastIndex=0;var result=re.test(text);re.lastIndex=0;return result}function toCodePoint(unicodeSurrogates,sep){var r=[],c=0,p=0,i=0;while(i tags or HTMLImage nodes. + * "emojify" a generic text or DOM Element. + * + * @overloads + * + * String replacement for `innerHTML` or server side operations + * twemoji.parse(string); + * twemoji.parse(string, Function); + * twemoji.parse(string, Object); + * + * HTMLElement tree parsing for safer operations over existing DOM + * twemoji.parse(HTMLElement); + * twemoji.parse(HTMLElement, Function); + * twemoji.parse(HTMLElement, Object); + * + * @param string|HTMLElement the source to parse and enrich with emoji. + * + * string replace emoji matches with tags. + * Mainly used to inject emoji via `innerHTML` + * It does **not** parse the string or validate it, + * it simply replaces found emoji with a tag. + * NOTE: be sure this won't affect security. + * + * HTMLElement walk through the DOM tree and find emoji + * that are inside **text node only** (nodeType === 3) + * Mainly used to put emoji in already generated DOM + * without compromising surrounding nodes and + * **avoiding** the usage of `innerHTML`. + * NOTE: Using DOM elements instead of strings should + * improve security without compromising too much + * performance compared with a less safe `innerHTML`. + * + * @param Function|Object [optional] + * either the callback that will be invoked or an object + * with all properties to use per each found emoji. + * + * Function if specified, this will be invoked per each emoji + * that has been found through the RegExp except + * those follwed by the invariant \uFE0E ("as text"). + * Once invoked, parameters will be: + * + * 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. + * + * + * Object if specified, an object containing the following properties + * + * callback Function the callback to invoke per each found emoji. + * base string the base url, by default twemoji.base + * ext string the image extension, by default twemoji.ext + * size string the assets size, by default twemoji.size + * + * @example + * + * twemoji.parse("I \u2764\uFE0F emoji!"); + * // I ❤️ emoji! + * + * + * twemoji.parse("I \u2764\uFE0F emoji!", function(iconId, options) { + * return '/assets/' + iconId + '.gif'; + * }); + * // I ❤️ emoji! + * + * + * twemoji.parse("I \u2764\uFE0F emoji!", { + * size: 72, + * callback: function(iconId, options) { + * return '/assets/' + options.size + '/' + iconId + options.ext; + * } + * }); + * // I ❤️ emoji! + * + */ + parse: parse, + + /** + * Given a string, invokes the callback argument + * per each emoji found in such string. + * This is the most raw version used by + * the .parse(string) method itself. + * + * @param string generic string to parse + * @param Function a generic callback that will be + * invoked to replace the content. + * This calback wil receive standard + * String.prototype.replace(str, callback) + * arguments such: + * callback( + * rawText, // the emoji match + * ); + * + * and others commonly received via replace. + */ + replace: replace, + + /** + * Simplify string tests against emoji. + * + * @param string some text that might contain emoji + * @return boolean true if any emoji was found, false otherwise. + * + * @example + * + * if (twemoji.test(someContent)) { + * console.log("emoji All The Things!"); + * } + */ + test: test + }, + + // used to escape HTML special chars in attributes + escaper = { + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }, + + // RegExp based on emoji's official Unicode standards + // http://www.unicode.org/Public/UNIDATA/EmojiSources.txt + re = /(?:[\u0039\u0038\u0037\u0036\u0035\u0034\u0033\u0032\u0031\u0030\u0023])\ufe0f?\u20e3|\ud83c\udde8\ud83c\uddf3|\ud83c\udde9\ud83c\uddea|\ud83c\uddea\ud83c\uddf8|\ud83c\uddeb\ud83c\uddf7|\ud83c\uddec\ud83c\udde7|\ud83c\uddee\ud83c\uddf9|\ud83c\uddef\ud83c\uddf5|\ud83c\uddf0\ud83c\uddf7|\ud83c\uddf7\ud83c\uddfa|\ud83c\uddfa\ud83c\uddf8|\ud83d[\udc00-\udc3e\udc40\udc42-\udcf7\udcf9-\udcfc\udd00-\udd3d\udd50-\udd67\uddfb-\ude42\ude45-\ude4f\ude80-\udec5]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf30-\udf35\udf37-\udf7c\udf80-\udf93\udfa0-\udfc4\udfc6-\udfca\udfe0-\udff0]|[\ue50a\u27bf\u27b0\u2797\u2796\u2795\u2755\u2754\u2753\u274e\u274c\u2728\u270b\u270a\u2705\u26ce\u23f3\u23f0\u23ec\u23eb\u23ea\u23e9]|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37]|[\u3299\u3297\u303d\u3030\u2b55\u2b50\u2b1c\u2b1b\u2b07\u2b06\u2b05\u2935\u2934\u27a1\u2764\u2757\u2747\u2744\u2734\u2733\u2716\u2714\u2712\u270f\u270c\u2709\u2708\u2702\u26fd\u26fa\u26f5\u26f3\u26f2\u26ea\u26d4\u26c5\u26c4\u26be\u26bd\u26ab\u26aa\u26a1\u26a0\u2693\u267f\u267b\u2668\u2666\u2665\u2663\u2660\u2653\u2652\u2651\u2650\u264f\u264e\u264d\u264c\u264b\u264a\u2649\u2648\u263a\u261d\u2615\u2614\u2611\u260e\u2601\u2600\u25fe\u25fd\u25fc\u25fb\u25c0\u25b6\u25ab\u25aa\u24c2\u231b\u231a\u21aa\u21a9\u2199\u2198\u2197\u2196\u2195\u2194\u2139\u2122\u2049\u203c\u00ae\u00a9])(?:\ufe0f|(?!\ufe0e))/g, + + // avoid runtime RegExp creation for not so smart, + // not JIT based, and old browsers / engines + UFE0Fg = /\uFE0F/g, + + // avoid using a string literal like '\u200D' here because minifiers expand it inline + U200D = String.fromCharCode(0x200D), + + // used to find HTML special chars in attributes + rescaper = /[&<>'"]/g, + + // nodes with type 1 which should **not** be parsed (including lower case svg) + shouldntBeParsed = /IFRAME|NOFRAMES|NOSCRIPT|SCRIPT|SELECT|STYLE|TEXTAREA|[a-z]/, + + // just a private shortcut + fromCharCode = String.fromCharCode; + + return twemoji; + + + ///////////////////////// + // private functions // + // declaration // + ///////////////////////// + + /** + * Shortcut to create text nodes + * @param string text used to create DOM text node + * @return Node a DOM node with that text + */ + function createText(text) { + return document.createTextNode(text); + } + + /** + * Utility function to escape html attribute text + * @param string text use in HTML attribute + * @return string text encoded to use in HTML attribute + */ + function escapeHTML(s) { + return s.replace(rescaper, replacer); + } + + /** + * Default callback used to generate emoji src + * based on Twitter CDN + * @param string the emoji codepoint string + * @param string the default size to use, i.e. "36x36" + * @return string the image source to use + */ + function defaultImageSrcGenerator(icon, options) { + return ''.concat(options.base, options.size, '/', icon, options.ext); + } + + /** + * Given a generic DOM nodeType 1, walk through all children + * and store every nodeType 3 (#text) found in the tree. + * @param Element a DOM Element with probably some text in it + * @param Array the list of previously discovered text nodes + * @return Array same list with new discovered nodes, if any + */ + function grabAllTextNodes(node, allText) { + var + childNodes = node.childNodes, + length = childNodes.length, + subnode, + nodeType; + while (length--) { + subnode = childNodes[length]; + nodeType = subnode.nodeType; + // parse emoji only in text nodes + if (nodeType === 3) { + // collect them to process emoji later + allText.push(subnode); + } + // ignore all nodes that are not type 1 or that + // should not be parsed as script, style, and others + else if (nodeType === 1 && !shouldntBeParsed.test(subnode.nodeName)) { + grabAllTextNodes(subnode, allText); + } + } + return allText; + } + + /** + * Used to both remove the possible variant + * and to convert utf16 into code points. + * If there is a zero-width-joiner (U+200D), leave the variants in. + * @param string the raw text of the emoji match + */ + function grabTheRightIcon(rawText) { + // if variant is present as \uFE0F + return toCodePoint(rawText.indexOf(U200D) < 0 ? + rawText.replace(UFE0Fg, '') : + rawText + ); + } + + /** + * DOM version of the same logic / parser: + * emojify all found sub-text nodes placing images node instead. + * @param Element generic DOM node with some text in some child node + * @param Object options containing info about how to parse + * + * .callback Function the callback to invoke per each found emoji. + * .base string the base url, by default twemoji.base + * .ext string the image extension, by default twemoji.ext + * .size string the assets size, by default twemoji.size + * + * @return Element same generic node with emoji in place, if any. + */ + function parseNode(node, options) { + var + allText = grabAllTextNodes(node, []), + length = allText.length, + attrib, + attrname, + modified, + fragment, + subnode, + text, + match, + i, + index, + img, + rawText, + iconId, + src; + while (length--) { + modified = false; + fragment = document.createDocumentFragment(); + subnode = allText[length]; + text = subnode.nodeValue; + i = 0; + while ((match = re.exec(text))) { + index = match.index; + if (index !== i) { + fragment.appendChild( + createText(text.slice(i, index)) + ); + } + 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 = rawText; + img.src = src; + modified = true; + fragment.appendChild(img); + } + if (!img) fragment.appendChild(createText(rawText)); + img = null; + } + // is there actually anything to replace in here ? + if (modified) { + // any text left to be added ? + if (i < text.length) { + fragment.appendChild( + createText(text.slice(i)) + ); + } + // replace the text node only, leave intact + // anything else surrounding such text + subnode.parentNode.replaceChild(fragment, subnode); + } + } + return node; + } + + /** + * String/HTML version of the same logic / parser: + * emojify a generic text placing images tags instead of surrogates pair. + * @param string generic string with possibly some emoji in it + * @param Object options containing info about how to parse + * + * .callback Function the callback to invoke per each found emoji. + * .base string the base url, by default twemoji.base + * .ext string the image extension, by default twemoji.ext + * .size string the assets size, by default twemoji.size + * + * @return the string with replacing all found and parsed emoji + */ + function parseString(str, options) { + return replace(str, function (rawText) { + var + ret = rawText, + iconId = grabTheRightIcon(rawText), + src = options.callback(iconId, options), + attrib, + attrname; + if (src) { + // recycle the match string replacing the emoji + // with its image counter part + ret = ''); + } + return ret; + }); + } + + /** + * Function used to actually replace HTML special chars + * @param string HTML special char + * @return string encoded HTML special char + */ + function replacer(m) { + return escaper[m]; + } + + /** + * Default options.attribute callback + * @return null + */ + function returnNull() { + return null; + } + + /** + * Given a generic value, creates its squared counterpart if it's a number. + * As example, number 36 will return '36x36'. + * @param any a generic value. + * @return any a string representing asset size, i.e. "36x36" + * only in case the value was a number. + * Returns initial value otherwise. + */ + function toSizeSquaredAsset(value) { + return typeof value === 'number' ? + value + 'x' + value : + value; + } + + + ///////////////////////// + // exported functions // + // declaration // + ///////////////////////// + + function fromCodePoint(codepoint) { + var code = typeof codepoint === 'string' ? + parseInt(codepoint, 16) : codepoint; + if (code < 0x10000) { + return fromCharCode(code); + } + code -= 0x10000; + return fromCharCode( + 0xD800 + (code >> 10), + 0xDC00 + (code & 0x3FF) + ); + } + + function parse(what, how) { + if (!how || typeof how === 'function') { + how = {callback: how}; + } + // if first argument is string, inject html tags + // otherwise use the DOM tree and parse text nodes only + return (typeof what === 'string' ? parseString : parseNode)(what, { + callback: how.callback || defaultImageSrcGenerator, + attributes: typeof how.attributes === 'function' ? how.attributes : returnNull, + base: typeof how.base === 'string' ? how.base : twemoji.base, + ext: how.ext || twemoji.ext, + size: how.folder || toSizeSquaredAsset(how.size || twemoji.size), + className: how.className || twemoji.className, + onerror: how.onerror || twemoji.onerror + }); + } + + function replace(text, callback) { + return String(text).replace(re, callback); + } + + function test(text) { + // IE6 needs a reset before too + re.lastIndex = 0; + var result = re.test(text); + re.lastIndex = 0; + return result; + } + + function toCodePoint(unicodeSurrogates, sep) { + var + r = [], + c = 0, + p = 0, + i = 0; + while (i < unicodeSurrogates.length) { + c = unicodeSurrogates.charCodeAt(i++); + if (p) { + r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16)); + p = 0; + } else if (0xD800 <= c && c <= 0xDBFF) { + p = c; + } else { + r.push(c.toString(16)); + } + } + return r.join(sep || '-'); + } + +}()); +if (!location.protocol) { + twemoji.base = twemoji.base.replace(/^http:/, ""); +} +module.exports = twemoji; \ No newline at end of file diff --git a/1/utils/create-dist b/1/utils/create-dist new file mode 100755 index 00000000..49c5b496 --- /dev/null +++ b/1/utils/create-dist @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +/*! Copyright Twitter Inc. and other contributors. Licensed under MIT *//* + https://github.com/twitter/twemoji/blob/gh-pages/LICENSE +*/ + +var fs = require('fs'); +var path = require('path'); + +function file(which) { + return path.join(__dirname, '..', which); +} + +fs.writeFileSync( + file('twemoji.npm.js'), + [ + 'var location = global.location || {};', + fs.readFileSync(file('twemoji.js')), + 'if (!location.protocol) {', + ' twemoji.base = twemoji.base.replace(/^http:/, "");', + '}', + 'module.exports = twemoji;' + ].join('\n') +); + +fs.writeFileSync( + file('twemoji.amd.js'), + 'define(function () {\n' + + fs.readFileSync(file('twemoji.js')).toString().replace( + /^(.)/gm, ' $1' + ) + + '\n return twemoji;\n});' +); + +require('child_process').spawn( + 'node', + [ + path.join(__dirname, '../../', 'node_modules/uglify-js/bin/uglifyjs'), + '--verbose', + file('twemoji.js'), + '-o', + file('twemoji.tmp.js') + ] +).on('close', function () { + fs.writeFileSync( + file('twemoji.min.js'), + '/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */\n' + + fs.readFileSync(file('twemoji.tmp.js')) + ); + fs.unlink(file('twemoji.tmp.js')); + // gzip -c twemoji.min.js | wc -c +}); \ No newline at end of file diff --git a/1/utils/generate b/1/utils/generate new file mode 100755 index 00000000..9f4a6d06 --- /dev/null +++ b/1/utils/generate @@ -0,0 +1,995 @@ +#!/usr/bin/env node + + /*! Copyright Twitter Inc. and other contributors. Licensed under MIT *//* + https://github.com/twitter/twemoji/blob/gh-pages/LICENSE + */ + +// dependencies +var fs = require('fs'); +var http = require('http'); +var path = require('path'); + +function file(which) { + return path.join(__dirname, '../..', which); +} + +// Twitter assets by property name +var assets = { + '72x72': [], + 'svg': [] +}; + +// white spaces we don't want to catch via the RegExp +// there is no asset equivalent for these +var ignoreMissing = ['2002', '2003', '2005']; + +// basic utility to organize async code +// see: http://webreflection.blogspot.co.uk/2012/03/tweet-sized-queue-system.html +// or: http://webreflection.blogspot.co.uk/2012/06/working-with-queues.html +function Queue(args, f) { + setTimeout(args.next = function next() { + return (f = args.shift()) ? !!f(args) || !0 : !1; + }, 0); + return args; +} + +// main task +Queue([ + + // will populate assets arrays + function grabAllAssets(q) { + console.log('analyzing all assets ... '); + // per each path/folder + Object.keys(assets).forEach(function (path, i, paths) { + // grab all files in that folder + fs.readdir(file(path), function (err, files) { + // and add them to the assets path + assets[path].push.apply( + assets[path], + files.map(upperCaseWithoutExtension) + ); + // once all assets arrays have been populated + if (paths.reduce(completed, true)) { + console.log('[INFO] assets contains ' + assets[path].length + ' emoji.'); + q.next(); + } + }); + }); + // drop extension + uppercase + function upperCaseWithoutExtension(file) { + return file.slice(0, file.lastIndexOf('.')).toUpperCase(); + } + // returns true if all assets have been populated + function completed(p, c) { + return p && assets[c].length; + } + }, + + // will fetch and store all emoji from unicode.org + function fetchEmojiSources(q) { + console.log('fetching EmojiSources.txt ... '); + // grab all emoji and test them against them + http.get("http://www.unicode.org/Public/UNIDATA/EmojiSources.txt", function (res) { + var chunks = []; + // if all good ... + if (res.statusCode === 200) { + // grab all data + res.on('data', chunks.push.bind(chunks)); + // once done ... + res.on('end', function () { + console.log('analyzing EmojiSources VS our assets ... '); + // store all missing assets in one object + var missing = {}; + // will be used to store an array with all missing + var missingGrouped = {}; + + // will be needed later on + // parse it, clean it, and store it once + q.emojiSource = chunks + .join('') + .split(/\r\n|\r|\n/) + // filter once + .filter(function (line) { + return this.test(line); + }, /^[0-9A-F]/) + // take only emoji info + .map(function (codePoint) { + return codePoint + .slice(0, codePoint.indexOf(';')) + .toUpperCase() + // drop spaces + .replace(/\s+/g, '-') + // drop 0 padded prefixes + .replace(/^0+/g, ''); + }); + + console.log('[INFO] parsed ' + q.emojiSource.length + ' standard emoji.'); + + // find out which one is missing from our assets + q.emojiSource.forEach( + function (emoji) { + // do not loop for emoji we know we should ignore + if (ignoreMissing.indexOf(emoji) < 0) { + // verify all others per each folder + this.forEach(function (path) { + if (assets[path].indexOf(emoji) < 0) { + (missing[path] || (missing[path] = [])).push(emoji); + missingGrouped[emoji] = true; + } + }); + } + + }, + // and per each folder + Object.keys(assets) + ); + + // if some missing emoji has been found + if (Object.keys(missing).length) { + // warn and show which one is missing + console.warn('[WARNING] missing assets for:'); + console.log(missing); + } + // create the array of all emoji we should ignore + q.ignore = ignoreMissing.concat(Object.keys(missingGrouped)); + + q.next(); + }); + } else { + console.error('[ERROR] unable to fetch emoji at unicode.org'); + process.exit(1); + } + }); + }, + + // grab the list of emoji that behave differently when + // variants such \uFE0E and \uFE0F are in place + function grabStandardVariants(q) { + console.log('fetching StandardizedVariants.txt ... '); + http.get( + "http://unicode.org/Public/UNIDATA/StandardizedVariants.txt", + function(res) { + var chunks = []; + if (res.statusCode == 200) { + res.on('data', chunks.push.bind(chunks)); + res.on('end', function () { + // cleaning up parsing sensitive emoji + q.variantsSensitive = chunks + .join('') // all content + .split(/\r\n|\r|\n/) // split in lines + .filter(function (line) { // containing FE0E; info + return this.test(line); // avoiding duplicated with FE0F + }, / FE0E; text style/) + .map(function (line) { // cleaned up to grab + return line.replace(this, '$1') // only first unicode + .toUpperCase(); // normalized as uppercase + }, /^([0-9A-F]{4,}) FE0E;.+$/) // sensitive char + ; + + // iOS keyboard allows U+002A U+FE0F U+20E3 even though not a standardized variant (yet?) + q.variantsSensitive.push('002A'); + // iOS keyboard allows U+2639 U+FE0F even though not a standardized variant (yet?) + q.variantsSensitive.push('2639'); + + console.log('[INFO] parsed ' + q.variantsSensitive.length + ' variant sensitive emoji.'); + q.next(); + + }); + } else { + console.error('[ERROR] unable to fetch standard variants at unicode.org'); + process.exit(1); + } + } + ); + }, + + // add our own assets that are not part of the Unicode standard + function addMissingEmoji(q) { + q.nonStandard = []; + Object.keys(assets).forEach(function (path, i) { + assets[path].forEach(function (emoji) { + if ( + q.emojiSource.indexOf(emoji) < 0 && + q.nonStandard.indexOf(emoji) < 0 + ) { + q.nonStandard.push(emoji); + } + }); + }); + + if (q.nonStandard.length) { + console.warn('[WARNING] assets contain ' + q.nonStandard.length + ' non standard emoji:'); + // console.log(q.nonStandard.join(', ')); + } + + q.emojiSource = q.emojiSource.concat(q.nonStandard) + q.next(); + }, + + // detect complete sets of five skin tones and a base + function detectDiversityEmoji(q) { + var isPresent = {}; + q.emojiSource.forEach(function (codePoints) { + isPresent[codePoints] = true; + }); + q.diversityBase = q.emojiSource.filter(function (codePoints) { + // Start with the set of Emoji with the light skin tone + return /-1F3FB$/.test(codePoints); + }).map(function (codePoints) { + // Take the skin tone off + return codePoints.replace(/-1F3FB$/, ''); + }).filter(function (baseCodePoints) { + // Verify that all other skin tones + no skin tone are present + return ['-1F3FC', '-1F3FD', '-1F3FE', '-1F3FF', ''].every(function (suffix) { + return isPresent[baseCodePoints + suffix]; + }); + }); + console.log('[INFO] parsed ' + q.diversityBase.length + ' diversity emoji.'); + q.next(); + }, + + // with all info, generate a RegExp that will catch + // only standard emoji that are present in our assets + function generateRegExp(q) { + console.log('generating a RegExp for available assets'); + var zwj = []; + var diversity = []; + var sensitive = []; + var sensitiveKeycaps = []; + var diversitySensitive = []; + var skinToneOptions = [ + '\\ud83c\\udffb', + '\\ud83c\\udffc', + '\\ud83c\\udffd', + '\\ud83c\\udffe', + '\\ud83c\\udfff' + ]; + var regular = []; + q.emojiSource.forEach(function (codePoints) { + var u; + var codePointsWithoutKeycap; + codePoints = codePoints.replace(/\b[A-F0-9]+\b/g, function (hex) { + // Pad all hex numbers to have at least 4 digits to match variantsSensitive + return hex.length < 4 ? ('000' + hex).slice(-4) : hex; + }); + if (q.ignore.indexOf(codePoints) < 0) { + u = toJSON(codePoints); + codePointsWithoutKeycap = codePoints.replace(/-20E3$/, ''); + if (codePoints.indexOf('200D') >= 0) { + zwj.push(u); + } else if (codePoints != codePointsWithoutKeycap && q.variantsSensitive.indexOf(codePointsWithoutKeycap) >= 0) { + sensitiveKeycaps.push(toJSON(codePointsWithoutKeycap)); + } else if (q.diversityBase.indexOf(codePoints.replace(/-1F3F[B-F]$/, '')) >= 0) { + // This is a diversity Emoji with or without a skin tone modifier + // Add it to the regex if this is the base without the modifier + if (q.diversityBase.indexOf(codePoints) >= 0) { + if (q.variantsSensitive.indexOf(codePoints) < 0) { + diversity.push(u); + } else { + diversitySensitive.push(u); + } + } + } else if (q.variantsSensitive.indexOf(codePoints) < 0) { + regular.push(u); + } else { + sensitive.push(u); + } + } + }); + + q.re = ''; + + // The Zero-width joiner Emojis, if present, need to come first + if (zwj.length) { + q.re += generateRegexPartial(zwj) + '|'; + } + + // Group the variant sensitive keycaps + if (sensitiveKeycaps.length) { + q.re += '(?:' + generateRegexPartial(sensitiveKeycaps) + ')\\ufe0f?\\u20e3|'; + } + + // Next, add the diversity enabled Emoji that may include a skin tone suffix + if (diversity.length + diversitySensitive.length) { + q.re += '(?:'; + if (diversitySensitive.length) { + // Some diversity are sensitive to variants + q.re += '(?:' + generateRegexPartial(diversitySensitive) + ')(?:\\ufe0f|(?!\\ufe0e))'; + if (diversity.length) { + q.re += '|'; + } + } + q.re += generateRegexPartial(diversity) + ')(?:' + generateRegexPartial(skinToneOptions) + '|)|'; + } + + // Next, the normal Emoji + q.re += generateRegexPartial(regular) + '|'; + + // Finally, add the rest of the sensitive ones that may be followed by U+FE0F but not U+FE0E + q.re += '(?:' + generateRegexPartial(sensitive) + ')(?:\\ufe0f|(?!\\ufe0e))'; + q.next(); + + // basic utilities to convert codepoints to JSON strings + function toJSON(codePoints) { + return codePoints.split('-').map(function (point) { + return UTF162JSON(fromCodePoint(point)); + }).join(''); + } + function fromCodePoint(codepoint) { + var code = typeof codepoint === 'string' ? + parseInt(codepoint, 16) : codepoint; + if (code < 0x10000) { + return String.fromCharCode(code); + } + code -= 0x10000; + return String.fromCharCode( + 0xD800 + (code >> 10), + 0xDC00 + (code & 0x3FF) + ); + } + function UTF162JSON(text) { + for (var i = 0, r = []; i < text.length; i++) { + r.push('\\u' + ('000' + text.charCodeAt(i).toString(16)).slice(-4)); + } + return r.join(''); + } + + // Items is an array of unicode sequences with \u escaping, like ["\u2963\ufe0f", "\u263a\ufe0f"] + // items get sorted by length (long to short), then unicode hex values (high to low) + // output is "or" ed together using | for regex + // ouput also combines adjacent items using character classes with ranges when they have common prefixes + // Example: "aab", "aac", "aad", "aag", "ba" becomes "aa[b-dg]|ba" + function generateRegexPartial(items) { + var currentPrefix = null; + var result = []; + var charClass = []; + var charRange = []; + items.map(function (item) { + // Convert from "\u2963\ufe0f" into ["2963", "fe0f"] + return item.split('\\u').slice(1); + }).sort(sortMethod).forEach(function (itemParts) { + var prefix = itemParts.slice(0, -1).join('\\u'); + if (prefix) { + prefix = '\\u' + prefix; + } + var suffix = itemParts.slice(-1); + if (prefix !== currentPrefix) { + flushCharClass(); + } + currentPrefix = prefix; + var suffixMinusOne = UTF162JSON(String.fromCharCode(parseInt(suffix, 16) - 1)); + + if (charRange.length && charRange.slice(-1)[0] !== suffixMinusOne) { + flushCharRange(); + } + charRange.push('\\u' + suffix); + }); + + flushCharClass(); + return result.join('|'); + + // a and b are arrays of hex UCS-2 units + function sortMethod(a, b) { + return !a.length ? 0 : + b.length - a.length || + parseInt(b[0], 16) - parseInt(a[0], 16) || + sortMethod(b.slice(1), a.slice(1) + ); + } + + function flushCharRange() { + charClass = charClass.concat((charRange.length < 3) ? + charRange : + [ charRange[0], '-', charRange.slice(-1)[0] ] + ); + charRange = []; + } + + function flushCharClass() { + flushCharRange(); + if (charClass.length) { + result.push(currentPrefix + (charClass.length == 1 ? + charClass[0] : + '[' + charClass.join('') + ']' + )); + } + charClass = []; + currentPrefix = null; + } + } + + }, + + function generateFile(q) { + console.log('generating ./twemoji.js'); + createTwemoji(q.re); + require('./create-dist'); + } + +]); + + + +function createTwemoji(re) { + fs.writeFileSync( + file('1/twemoji.js'), + '/*jslint indent: 2, browser: true, bitwise: true, plusplus: true */\n' + + 'var twemoji = (' + + function ( + /*! Copyright Twitter Inc. and other contributors. Licensed under MIT *//* + https://github.com/twitter/twemoji/blob/gh-pages/LICENSE + */ + + // WARNING: this file is generated automatically via + // `node twemoji-generator.js` + // please update its `createTwemoji` function + // at the bottom of the same file instead. + + ) { + 'use strict'; + + /*jshint maxparams:4 */ + + var + // the exported module object + twemoji = { + + + ///////////////////////// + // properties // + ///////////////////////// + + // default assets url, by default will be Twitter Inc. CDN + base: 'https://twemoji.maxcdn.com/1/', + + // default assets file extensions, by default '.png' + ext: '.png', + + // default assets/folder size, by default "72x72" + // available via Twitter CDN: 72 + size: '72x72', + + // default class name, by default 'emoji' + className: 'emoji', + + // basic utilities / helpers to convert code points + // to JavaScript surrogates and vice versa + convert: { + + /** + * Given an HEX codepoint, returns UTF16 surrogate pairs. + * + * @param string generic codepoint, i.e. '1F4A9' + * @return string codepoint transformed into utf16 surrogates pair, + * i.e. \uD83D\uDCA9 + * + * @example + * twemoji.convert.fromCodePoint('1f1e8'); + * // "\ud83c\udde8" + * + * '1f1e8-1f1f3'.split('-').map(twemoji.convert.fromCodePoint).join('') + * // "\ud83c\udde8\ud83c\uddf3" + */ + fromCodePoint: fromCodePoint, + + /** + * Given UTF16 surrogate pairs, returns the equivalent HEX codepoint. + * + * @param string generic utf16 surrogates pair, i.e. \uD83D\uDCA9 + * @param string optional separator for double code points, default='-' + * @return string utf16 transformed into codepoint, i.e. '1F4A9' + * + * @example + * twemoji.convert.toCodePoint('\ud83c\udde8\ud83c\uddf3'); + * // "1f1e8-1f1f3" + * + * twemoji.convert.toCodePoint('\ud83c\udde8\ud83c\uddf3', '~'); + * // "1f1e8~1f1f3" + */ + toCodePoint: toCodePoint + }, + + + ///////////////////////// + // methods // + ///////////////////////// + + /** + * User first: used to remove missing images + * preserving the original text intent when + * a fallback for network problems is desired. + * Automatically added to Image nodes via DOM + * It could be recycled for string operations via: + * $('img.emoji').on('error', twemoji.onerror) + */ + onerror: function onerror() { + if (this.parentNode) { + this.parentNode.replaceChild(createText(this.alt), this); + } + }, + + /** + * Main method/logic to generate either tags or HTMLImage nodes. + * "emojify" a generic text or DOM Element. + * + * @overloads + * + * String replacement for `innerHTML` or server side operations + * twemoji.parse(string); + * twemoji.parse(string, Function); + * twemoji.parse(string, Object); + * + * HTMLElement tree parsing for safer operations over existing DOM + * twemoji.parse(HTMLElement); + * twemoji.parse(HTMLElement, Function); + * twemoji.parse(HTMLElement, Object); + * + * @param string|HTMLElement the source to parse and enrich with emoji. + * + * string replace emoji matches with tags. + * Mainly used to inject emoji via `innerHTML` + * It does **not** parse the string or validate it, + * it simply replaces found emoji with a tag. + * NOTE: be sure this won't affect security. + * + * HTMLElement walk through the DOM tree and find emoji + * that are inside **text node only** (nodeType === 3) + * Mainly used to put emoji in already generated DOM + * without compromising surrounding nodes and + * **avoiding** the usage of `innerHTML`. + * NOTE: Using DOM elements instead of strings should + * improve security without compromising too much + * performance compared with a less safe `innerHTML`. + * + * @param Function|Object [optional] + * either the callback that will be invoked or an object + * with all properties to use per each found emoji. + * + * Function if specified, this will be invoked per each emoji + * that has been found through the RegExp except + * those follwed by the invariant \uFE0E ("as text"). + * Once invoked, parameters will be: + * + * 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. + * + * + * Object if specified, an object containing the following properties + * + * callback Function the callback to invoke per each found emoji. + * base string the base url, by default twemoji.base + * ext string the image extension, by default twemoji.ext + * size string the assets size, by default twemoji.size + * + * @example + * + * twemoji.parse("I \u2764\uFE0F emoji!"); + * // I ❤️ emoji! + * + * + * twemoji.parse("I \u2764\uFE0F emoji!", function(iconId, options) { + * return '/assets/' + iconId + '.gif'; + * }); + * // I ❤️ emoji! + * + * + * twemoji.parse("I \u2764\uFE0F emoji!", { + * size: 72, + * callback: function(iconId, options) { + * return '/assets/' + options.size + '/' + iconId + options.ext; + * } + * }); + * // I ❤️ emoji! + * + */ + parse: parse, + + /** + * Given a string, invokes the callback argument + * per each emoji found in such string. + * This is the most raw version used by + * the .parse(string) method itself. + * + * @param string generic string to parse + * @param Function a generic callback that will be + * invoked to replace the content. + * This calback wil receive standard + * String.prototype.replace(str, callback) + * arguments such: + * callback( + * rawText, // the emoji match + * ); + * + * and others commonly received via replace. + */ + replace: replace, + + /** + * Simplify string tests against emoji. + * + * @param string some text that might contain emoji + * @return boolean true if any emoji was found, false otherwise. + * + * @example + * + * if (twemoji.test(someContent)) { + * console.log("emoji All The Things!"); + * } + */ + test: test + }, + + // used to escape HTML special chars in attributes + escaper = { + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }, + + // RegExp based on emoji's official Unicode standards + // http://www.unicode.org/Public/UNIDATA/EmojiSources.txt + re = /twemoji/, + + // avoid runtime RegExp creation for not so smart, + // not JIT based, and old browsers / engines + UFE0Fg = /\uFE0F/g, + + // avoid using a string literal like '\u200D' here because minifiers expand it inline + U200D = String.fromCharCode(0x200D), + + // used to find HTML special chars in attributes + rescaper = /[&<>'"]/g, + + // nodes with type 1 which should **not** be parsed (including lower case svg) + shouldntBeParsed = /IFRAME|NOFRAMES|NOSCRIPT|SCRIPT|SELECT|STYLE|TEXTAREA|[a-z]/, + + // just a private shortcut + fromCharCode = String.fromCharCode; + + return twemoji; + + + ///////////////////////// + // private functions // + // declaration // + ///////////////////////// + + /** + * Shortcut to create text nodes + * @param string text used to create DOM text node + * @return Node a DOM node with that text + */ + function createText(text) { + return document.createTextNode(text); + } + + /** + * Utility function to escape html attribute text + * @param string text use in HTML attribute + * @return string text encoded to use in HTML attribute + */ + function escapeHTML(s) { + return s.replace(rescaper, replacer); + } + + /** + * Default callback used to generate emoji src + * based on Twitter CDN + * @param string the emoji codepoint string + * @param string the default size to use, i.e. "36x36" + * @return string the image source to use + */ + function defaultImageSrcGenerator(icon, options) { + return ''.concat(options.base, options.size, '/', icon, options.ext); + } + + /** + * Given a generic DOM nodeType 1, walk through all children + * and store every nodeType 3 (#text) found in the tree. + * @param Element a DOM Element with probably some text in it + * @param Array the list of previously discovered text nodes + * @return Array same list with new discovered nodes, if any + */ + function grabAllTextNodes(node, allText) { + var + childNodes = node.childNodes, + length = childNodes.length, + subnode, + nodeType; + while (length--) { + subnode = childNodes[length]; + nodeType = subnode.nodeType; + // parse emoji only in text nodes + if (nodeType === 3) { + // collect them to process emoji later + allText.push(subnode); + } + // ignore all nodes that are not type 1 or that + // should not be parsed as script, style, and others + else if (nodeType === 1 && !shouldntBeParsed.test(subnode.nodeName)) { + grabAllTextNodes(subnode, allText); + } + } + return allText; + } + + /** + * Used to both remove the possible variant + * and to convert utf16 into code points. + * If there is a zero-width-joiner (U+200D), leave the variants in. + * @param string the raw text of the emoji match + */ + function grabTheRightIcon(rawText) { + // if variant is present as \uFE0F + return toCodePoint(rawText.indexOf(U200D) < 0 ? + rawText.replace(UFE0Fg, '') : + rawText + ); + } + + /** + * DOM version of the same logic / parser: + * emojify all found sub-text nodes placing images node instead. + * @param Element generic DOM node with some text in some child node + * @param Object options containing info about how to parse + * + * .callback Function the callback to invoke per each found emoji. + * .base string the base url, by default twemoji.base + * .ext string the image extension, by default twemoji.ext + * .size string the assets size, by default twemoji.size + * + * @return Element same generic node with emoji in place, if any. + */ + function parseNode(node, options) { + var + allText = grabAllTextNodes(node, []), + length = allText.length, + attrib, + attrname, + modified, + fragment, + subnode, + text, + match, + i, + index, + img, + rawText, + iconId, + src; + while (length--) { + modified = false; + fragment = document.createDocumentFragment(); + subnode = allText[length]; + text = subnode.nodeValue; + i = 0; + while ((match = re.exec(text))) { + index = match.index; + if (index !== i) { + fragment.appendChild( + createText(text.slice(i, index)) + ); + } + 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 = rawText; + img.src = src; + modified = true; + fragment.appendChild(img); + } + if (!img) fragment.appendChild(createText(rawText)); + img = null; + } + // is there actually anything to replace in here ? + if (modified) { + // any text left to be added ? + if (i < text.length) { + fragment.appendChild( + createText(text.slice(i)) + ); + } + // replace the text node only, leave intact + // anything else surrounding such text + subnode.parentNode.replaceChild(fragment, subnode); + } + } + return node; + } + + /** + * String/HTML version of the same logic / parser: + * emojify a generic text placing images tags instead of surrogates pair. + * @param string generic string with possibly some emoji in it + * @param Object options containing info about how to parse + * + * .callback Function the callback to invoke per each found emoji. + * .base string the base url, by default twemoji.base + * .ext string the image extension, by default twemoji.ext + * .size string the assets size, by default twemoji.size + * + * @return the string with replacing all found and parsed emoji + */ + function parseString(str, options) { + return replace(str, function (rawText) { + var + ret = rawText, + iconId = grabTheRightIcon(rawText), + src = options.callback(iconId, options), + attrib, + attrname; + if (src) { + // recycle the match string replacing the emoji + // with its image counter part + ret = ''); + } + return ret; + }); + } + + /** + * Function used to actually replace HTML special chars + * @param string HTML special char + * @return string encoded HTML special char + */ + function replacer(m) { + return escaper[m]; + } + + /** + * Default options.attribute callback + * @return null + */ + function returnNull() { + return null; + } + + /** + * Given a generic value, creates its squared counterpart if it's a number. + * As example, number 36 will return '36x36'. + * @param any a generic value. + * @return any a string representing asset size, i.e. "36x36" + * only in case the value was a number. + * Returns initial value otherwise. + */ + function toSizeSquaredAsset(value) { + return typeof value === 'number' ? + value + 'x' + value : + value; + } + + + ///////////////////////// + // exported functions // + // declaration // + ///////////////////////// + + function fromCodePoint(codepoint) { + var code = typeof codepoint === 'string' ? + parseInt(codepoint, 16) : codepoint; + if (code < 0x10000) { + return fromCharCode(code); + } + code -= 0x10000; + return fromCharCode( + 0xD800 + (code >> 10), + 0xDC00 + (code & 0x3FF) + ); + } + + function parse(what, how) { + if (!how || typeof how === 'function') { + how = {callback: how}; + } + // if first argument is string, inject html tags + // otherwise use the DOM tree and parse text nodes only + return (typeof what === 'string' ? parseString : parseNode)(what, { + callback: how.callback || defaultImageSrcGenerator, + attributes: typeof how.attributes === 'function' ? how.attributes : returnNull, + base: typeof how.base === 'string' ? how.base : twemoji.base, + ext: how.ext || twemoji.ext, + size: how.folder || toSizeSquaredAsset(how.size || twemoji.size), + className: how.className || twemoji.className, + onerror: how.onerror || twemoji.onerror + }); + } + + function replace(text, callback) { + return String(text).replace(re, callback); + } + + function test(text) { + // IE6 needs a reset before too + re.lastIndex = 0; + var result = re.test(text); + re.lastIndex = 0; + return result; + } + + function toCodePoint(unicodeSurrogates, sep) { + var + r = [], + c = 0, + p = 0, + i = 0; + while (i < unicodeSurrogates.length) { + c = unicodeSurrogates.charCodeAt(i++); + if (p) { + r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16)); + p = 0; + } else if (0xD800 <= c && c <= 0xDBFF) { + p = c; + } else { + r.push(c.toString(16)); + } + } + return r.join(sep || '-'); + } + + }.toString() + // drop current indentation + .replace(/^ /gm, '') + // add the RegExp in the right place + .replace('re = /twemoji/', 're = /' + re + '/g') + // add the full license + .replace('/*! (C) Twitter Inc. */', + '/*! (C) Twitter Inc. *//*\n' + + fs.readFileSync(path.join(__dirname, '../../', 'LICENSE')).toString().replace( + /^./gm, ' ' + ) + + '\n */' + ) + '());'); +} diff --git a/1/utils/preview b/1/utils/preview new file mode 100755 index 00000000..40a6e4d3 --- /dev/null +++ b/1/utils/preview @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +/*! Copyright Twitter Inc. and other contributors. Licensed under MIT *//* + https://github.com/twitter/twemoji/blob/gh-pages/LICENSE +*/ + +// dependencies +var fs = require('fs'); +var path = require('path'); + +function file(which) { + return path.join(__dirname, '..', which); +} + +fs.readdir(file('../assets'), function (err, files) { + var page = fs.readFileSync(file('templates/preview.html')).toString().replace( + '{{emoji-list}}', + '
  • ' + files.map(function (file) { + return file.replace('.ai', '').split('-').map(function (hex) { + return '&#x' + hex.toUpperCase() + ';'; + }).join(''); + }).join('
  • \n
  • ')+ '
  • ' + ); + fs.writeFileSync( + file('test/preview.html'), + page.replace( + '{{emoji-options}}', + JSON.stringify({ + size: 72 + }) + ) + ); + fs.writeFileSync( + file('test/preview-svg.html'), + page.replace( + '{{emoji-options}}', + JSON.stringify({ + folder: '../svg', + ext: '.svg', + base: '' + }) + ) + ); +}); \ No newline at end of file diff --git a/1/utils/size b/1/utils/size new file mode 100755 index 00000000..4dd4ca67 --- /dev/null +++ b/1/utils/size @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +echo "twemoji.js $(cat 1/twemoji.js | wc -c)" +echo "twemoji.js gzipped $(gzip -c 1/twemoji.js | wc -c)" +echo "twemoji.min.js $(cat 1/twemoji.min.js | wc -c)" +echo "twemoji.min.js gzipped $(gzip -c 1/twemoji.min.js | wc -c)"