diff --git a/Periscope_Web_Client.meta.js b/Periscope_Web_Client.meta.js index 6679f0b..45cfb94 100644 --- a/Periscope_Web_Client.meta.js +++ b/Periscope_Web_Client.meta.js @@ -5,7 +5,7 @@ // @description Periscope client based on API requests. Visit example.net for launch. // @include https://api.twitter.com/oauth/openperiscope* // @include http://example.net/* -// @version 1.2 +// @version 1.3 // @author Pmmlabs@github // @grant GM_xmlhttpRequest // @connect periscope.tv diff --git a/Periscope_Web_Client.user.js b/Periscope_Web_Client.user.js index d6f8fbb..49e2f82 100644 --- a/Periscope_Web_Client.user.js +++ b/Periscope_Web_Client.user.js @@ -1,11 +1,11 @@ -// ==UserScript== +// ==UserScript== // @id OpenPeriscope@pmmlabs.ru // @name Periscope Web Client // @namespace https://greasyfork.org/users/23 // @description Periscope client based on API requests. Visit example.net for launch. // @include https://api.twitter.com/oauth/openperiscope* // @include http://example.net/* -// @version 1.2 +// @version 1.3 // @author Pmmlabs@github // @grant GM_xmlhttpRequest // @connect periscope.tv @@ -22,7 +22,7 @@ // @noframes // ==/UserScript== -var emoji = emoji || new EmojiConvertor(); // js-emoji 3.0 upgrade +var emoji = new EmojiConvertor(); NODEJS = typeof GM_xmlhttpRequest == 'undefined'; var IMG_PATH = 'https://raw.githubusercontent.com/Pmmlabs/OpenPeriscope/master'; if (NODEJS) { // for NW.js diff --git a/README.md b/README.md index ce8696e..ddd49d8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,12 @@ Unofficial in-browser web client for Periscope (userscript) ### Using as standalone application -If you have [NW.js](http://nwjs.io) installed, you can run ` nw . ` in repo directory +If you have [NW.js](http://nwjs.io) installed, you can run +``` + npm install + nw . + ``` + in repo directory Or, you can use pre-built executables from [Releases page](https://github.com/Pmmlabs/OpenPeriscope/releases) diff --git a/emoji.js b/emoji.js index d3979b7..758753c 100644 --- a/emoji.js +++ b/emoji.js @@ -1,92 +1,120 @@ -;(function(local_setup) { +"use strict"; -/** - * @global - * @namespace - */ -function emoji(){} - /** - * The set of images to use for graphical emoji. - * - * @memberof emoji - * @type {string} - */ - emoji.img_set = 'apple'; +;(function() { - /** - * Configuration details for different image sets. This includes a path to a directory containing the - * individual images (`path`) and a URL to sprite sheets (`sheet`). All of these images can be found - * in the [emoji-data repository]{@link https://github.com/iamcal/emoji-data}. Using a CDN for these - * is not a bad idea. - * - * @memberof emoji - * @type { - */ - emoji.img_sets = { - 'apple' : {'path' : '/emoji-data/img-apple-64/' , 'sheet' : '/emoji-data/sheet_apple_64.png', 'mask' : 1 }, - 'google' : {'path' : '/emoji-data/img-google-64/' , 'sheet' : '/emoji-data/sheet_google_64.png', 'mask' : 2 }, - 'twitter' : {'path' : '/emoji-data/img-twitter-64/' , 'sheet' : '/emoji-data/sheet_twitter_64.png', 'mask' : 4 }, - 'emojione' : {'path' : '/emoji-data/img-emojione-64/', 'sheet' : '/emoji-data/sheet_emojione_64.png', 'mask' : 8 } - }; + var root = this; + var previous_emoji = root.EmojiConvertor; - /** - * Use a CSS class instead of specifying a sprite or background image for - * the span representing the emoticon. This requires a CSS sheet with - * emoticon data-uris. - * - * @memberof emoji - * @type bool - * @todo document how to build the CSS stylesheet this requires. - */ - emoji.use_css_imgs = false; /** - * Instead of replacing emoticons with the appropriate representations, - * replace them with their colon string representation. - * @memberof emoji - * @type bool + * @global + * @namespace */ - emoji.colons_mode = false; - emoji.text_mode = false; - /** - * If true, sets the "title" property on the span or image that gets - * inserted for the emoticon. - * @memberof emoji - * @type bool - */ - emoji.include_title = false; + var emoji = function(){ - /** - * If the platform supports native emoticons, use those instead - * of the fallbacks. - * @memberof emoji - * @type bool - */ - emoji.allow_native = true; + var self = this; - /** - * Set to true to use CSS sprites instead of individual images on - * platforms that support it. - * - * @memberof emoji - * @type bool - */ - emoji.use_sheet = false; + /** + * The set of images to use for graphical emoji. + * + * @memberof emoji + * @type {string} + */ + self.img_set = 'apple'; - /** - * - * Set to true to avoid black & white native Windows emoji being used. - * - * @memberof emoji - * @type bool - */ - emoji.avoid_ms_emoji = true; + /** + * Configuration details for different image sets. This includes a path to a directory containing the + * individual images (`path`) and a URL to sprite sheets (`sheet`). All of these images can be found + * in the [emoji-data repository]{@link https://github.com/iamcal/emoji-data}. Using a CDN for these + * is not a bad idea. + * + * @memberof emoji + * @type { + */ + self.img_sets = { + 'apple' : {'path' : '/emoji-data/img-apple-64/' , 'sheet' : '/emoji-data/sheet_apple_64.png', 'mask' : 1 }, + 'google' : {'path' : '/emoji-data/img-google-64/' , 'sheet' : '/emoji-data/sheet_google_64.png', 'mask' : 2 }, + 'twitter' : {'path' : '/emoji-data/img-twitter-64/' , 'sheet' : '/emoji-data/sheet_twitter_64.png', 'mask' : 4 }, + 'emojione' : {'path' : '/emoji-data/img-emojione-64/', 'sheet' : '/emoji-data/sheet_emojione_64.png', 'mask' : 8 } + }; + + /** + * Use a CSS class instead of specifying a sprite or background image for + * the span representing the emoticon. This requires a CSS sheet with + * emoticon data-uris. + * + * @memberof emoji + * @type bool + * @todo document how to build the CSS stylesheet self requires. + */ + self.use_css_imgs = false; + + /** + * Instead of replacing emoticons with the appropriate representations, + * replace them with their colon string representation. + * @memberof emoji + * @type bool + */ + self.colons_mode = false; + self.text_mode = false; + + /** + * If true, sets the "title" property on the span or image that gets + * inserted for the emoticon. + * @memberof emoji + * @type bool + */ + self.include_title = false; + + /** + * If the platform supports native emoticons, use those instead + * of the fallbacks. + * @memberof emoji + * @type bool + */ + self.allow_native = true; + + /** + * Set to true to use CSS sprites instead of individual images on + * platforms that support it. + * + * @memberof emoji + * @type bool + */ + self.use_sheet = false; + + /** + * + * Set to true to avoid black & white native Windows emoji being used. + * + * @memberof emoji + * @type bool + */ + self.avoid_ms_emoji = true; + + /** + * + * Set to true to allow :CAPITALIZATION: + * + * @memberof emoji + * @type bool + */ + self.allow_caps = false; + + // Keeps track of what has been initialized. + /** @private */ + self.inits = {}; + self.map = {}; + + return self; + } + + emoji.prototype.noConflict = function(){ + root.EmojiConvertor = previous_emoji; + return emoji; + } - // Keeps track of what has been initialized. - /** @private */ - emoji.inits = {}; - emoji.map = {}; /** * @memberof emoji @@ -97,12 +125,10 @@ function emoji(){} * replaced by a representatation that's supported by the current * environtment. */ - emoji.replace_emoticons = function(str){ - emoji.init_emoticons(); - return str.replace(emoji.rx_emoticons, function(m, $1, $2){ - var val = emoji.map.emoticons[$2]; - return val ? $1+emoji.replacement(val, $2) : m; - }); + emoji.prototype.replace_emoticons = function(str){ + var self = this; + var colonized = self.replace_emoticons_with_colons(str); + return self.replace_colons(colonized); }; /** @@ -113,12 +139,63 @@ function emoji(){} * @returns {string} A new string with all emoticons in `str` * replaced by their colon string representations (ie. `:smile:`) */ - emoji.replace_emoticons_with_colons = function(str){ - emoji.init_emoticons(); - return str.replace(emoji.rx_emoticons, function(m, $1, $2){ - var val = emoji.data[emoji.map.emoticons[$2]][3][0]; + emoji.prototype.replace_emoticons_with_colons = function(str){ + var self = this; + self.init_emoticons(); + var _prev_offset = 0; + var emoticons_with_parens = []; + var str_replaced = str.replace(self.rx_emoticons, function(m, $1, emoticon, offset){ + var prev_offset = _prev_offset; + _prev_offset = offset + m.length; + + var has_open_paren = emoticon.indexOf('(') !== -1; + var has_close_paren = emoticon.indexOf(')') !== -1; + + /* + * Track paren-having emoticons for fixing later + */ + if ((has_open_paren || has_close_paren) && emoticons_with_parens.indexOf(emoticon) == -1) { + emoticons_with_parens.push(emoticon); + } + + /* + * Look for preceding open paren for emoticons that contain a close paren + * This prevents matching "8)" inside "(around 7 - 8)" + */ + if (has_close_paren && !has_open_paren) { + var piece = str.substring(prev_offset, offset); + if (piece.indexOf('(') !== -1 && piece.indexOf(')') === -1) return m; + } + + /* + * See if we're in a numbered list + * This prevents matching "8)" inside "7) foo\n8) bar" + */ + if (m === '\n8)') { + var before_match = str.substring(0, offset); + if (/\n?(6\)|7\))/.test(before_match)) return m; + } + + var val = self.data[self.map.emoticons[emoticon]][3][0]; return val ? $1+':'+val+':' : m; }); + + /* + * Come back and fix emoticons we ignored because they were inside parens. + * It's useful to do self at the end so we don't get tripped up by other, + * normal emoticons + */ + if (emoticons_with_parens.length) { + var escaped_emoticons = emoticons_with_parens.map(self.escape_rx); + var parenthetical_rx = new RegExp('(\\(.+)('+escaped_emoticons.join('|')+')(.+\\))', 'g'); + + str_replaced = str_replaced.replace(parenthetical_rx, function(m, $1, emoticon, $2) { + var val = self.data[self.map.emoticons[emoticon]][3][0]; + return val ? $1+':'+val+':'+$2 : m; + }); + } + + return str_replaced; }; /** @@ -129,34 +206,36 @@ function emoji(){} * @returns {string} A new string with all colon string emoticons replaced * with the appropriate representation. */ - emoji.replace_colons = function(str){ - emoji.init_colons(); + emoji.prototype.replace_colons = function(str){ + var self = this; + self.init_colons(); - return str.replace(emoji.rx_colons, function(m){ + return str.replace(self.rx_colons, function(m){ var idx = m.substr(1, m.length-2); + if (self.allow_caps) idx = idx.toLowerCase(); // special case - an emoji with a skintone modified if (idx.indexOf('::skin-tone-') > -1){ var skin_tone = idx.substr(-1, 1); var skin_idx = 'skin-tone-'+skin_tone; - var skin_val = emoji.map.colons[skin_idx]; + var skin_val = self.map.colons[skin_idx]; idx = idx.substr(0, idx.length - 13); - var val = emoji.map.colons[idx]; + var val = self.map.colons[idx]; if (val){ - return emoji.replacement(val, idx, ':', { + return self.replacement(val, idx, ':', { 'idx' : skin_val, 'actual' : skin_idx, 'wrapper' : ':' }); }else{ - return ':' + idx + ':' + emoji.replacement(skin_val, skin_idx, ':'); + return ':' + idx + ':' + self.replacement(skin_val, skin_idx, ':'); } }else{ - var val = emoji.map.colons[idx]; - return val ? emoji.replacement(val, idx, ':') : m; + var val = self.map.colons[idx]; + return val ? self.replacement(val, idx, ':') : m; } }); }; @@ -169,10 +248,11 @@ function emoji(){} * @returns {string} A new string with all unicode emoticons replaced with * the appropriate representation for the current environment. */ - emoji.replace_unified = function(str){ - emoji.init_unified(); - return str.replace(emoji.rx_unified, function(m, p1, p2){ - var val = emoji.map.unified[p1]; + emoji.prototype.replace_unified = function(str){ + var self = this; + self.init_unified(); + return str.replace(self.rx_unified, function(m, p1, p2){ + var val = self.map.unified[p1]; if (!val) return m; var idx = null; if (p2 == '\uD83C\uDFFB') idx = '1f3fb'; @@ -181,19 +261,20 @@ function emoji(){} if (p2 == '\uD83C\uDFFE') idx = '1f3fe'; if (p2 == '\uD83C\uDFFF') idx = '1f3ff'; if (idx){ - return emoji.replacement(val, null, null, { + return self.replacement(val, null, null, { idx : idx, actual : p2, wrapper : '' }); } - return emoji.replacement(val); + return self.replacement(val); }); }; // Does the actual replacement of a character with the appropriate /** @private */ - emoji.replacement = function(idx, actual, wrapper, variation){ + emoji.prototype.replacement = function(idx, actual, wrapper, variation){ + var self = this; // for emoji with variation modifiers, set `etxra` to the standalone output for the // modifier (used if we can't combine the glyph) and set variation_idx to key of the @@ -201,55 +282,64 @@ function emoji(){} var extra = ''; var variation_idx = 0; if (typeof variation === 'object'){ - extra = emoji.replacement(variation.idx, variation.actual, variation.wrapper); + extra = self.replacement(variation.idx, variation.actual, variation.wrapper); variation_idx = idx + '-' + variation.idx; } + var img_set = self.img_set; + + // When not using sheets (which all contain all emoji), + // make sure we use an img_set that contains this emoji. + // For now, assume set "apple" has all individual images. + if ((!self.use_sheet || !self.supports_css) && !(self.data[idx][6] & self.img_sets[self.img_set].mask)) { + img_set = 'apple'; + } + // deal with simple modes (colons and text) first wrapper = wrapper || ''; - if (emoji.colons_mode) return ':'+emoji.data[idx][3][0]+':'+extra; - var text_name = (actual) ? wrapper+actual+wrapper : emoji.data[idx][8] || wrapper+emoji.data[idx][3][0]+wrapper; - if (emoji.text_mode) return text_name + extra; + if (self.colons_mode) return ':'+self.data[idx][3][0]+':'+extra; + var text_name = (actual) ? wrapper+actual+wrapper : self.data[idx][8] || wrapper+self.data[idx][3][0]+wrapper; + if (self.text_mode) return text_name + extra; // native modes next. // for variations selectors, we just need to output them raw, which `extra` will contain. - emoji.init_env(); - if (emoji.replace_mode == 'unified' && emoji.allow_native && emoji.data[idx][0][0]) return emoji.data[idx][0][0] + extra; - if (emoji.replace_mode == 'softbank' && emoji.allow_native && emoji.data[idx][1]) return emoji.data[idx][1] + extra; - if (emoji.replace_mode == 'google' && emoji.allow_native && emoji.data[idx][2]) return emoji.data[idx][2] + extra; + self.init_env(); + if (self.replace_mode == 'unified' && self.allow_native && self.data[idx][0][0]) return self.data[idx][0][0] + extra; + if (self.replace_mode == 'softbank' && self.allow_native && self.data[idx][1]) return self.data[idx][1] + extra; + if (self.replace_mode == 'google' && self.allow_native && self.data[idx][2]) return self.data[idx][2] + extra; // finally deal with image modes. // variation selectors are more complex here - if the image set and particular emoji supports variations, then // use the variation image. otherwise, return it as a separate image (already calculated in `extra`). // first we set up the params we'll use if we can't use a variation. - var img = emoji.data[idx][7] || emoji.img_sets[emoji.img_set].path+idx+'.png'; - var title = emoji.include_title ? ' title="'+(actual || emoji.data[idx][3][0])+'"' : ''; - var text = emoji.include_text ? wrapper+(actual || emoji.data[idx][3][0])+wrapper : ''; - var px = emoji.data[idx][4]; - var py = emoji.data[idx][5]; + var img = self.data[idx][7] || self.img_sets[img_set].path+idx+'.png'; + var title = self.include_title ? ' title="'+(actual || self.data[idx][3][0])+'"' : ''; + var text = self.include_text ? wrapper+(actual || self.data[idx][3][0])+wrapper : ''; + var px = self.data[idx][4]; + var py = self.data[idx][5]; // now we'll see if we can use a varition. if we can, we can override the params above and blank // out `extra` so we output a sinlge glyph. // we need to check that: // * we requested a variation // * such a variation exists in `emoji.variations_data` - // * we're not using a custom image for this glyph + // * we're not using a custom image for self glyph // * the variation has an image defined for the current image set - if (variation_idx && emoji.variations_data[variation_idx] && emoji.variations_data[variation_idx][2] && !emoji.data[idx][9]){ - if (emoji.variations_data[variation_idx][2] & emoji.img_sets[emoji.img_set].mask){ - img = emoji.img_sets[emoji.img_set].path+variation_idx+'.png'; - px = emoji.variations_data[variation_idx][0]; - py = emoji.variations_data[variation_idx][1]; + if (variation_idx && self.variations_data[variation_idx] && self.variations_data[variation_idx][2] && !self.data[idx][7]){ + if (self.variations_data[variation_idx][2] & self.img_sets[self.img_set].mask){ + img = self.img_sets[self.img_set].path+variation_idx+'.png'; + px = self.variations_data[variation_idx][0]; + py = self.variations_data[variation_idx][1]; extra = ''; } } - if (emoji.supports_css) { - if (emoji.use_sheet && px != null && py != null){ - var mul = 100 / (emoji.sheet_size - 1); - var style = 'background: url('+emoji.img_sets[emoji.img_set].sheet+');background-position:'+(mul*px)+'% '+(mul*py)+'%;background-size:'+emoji.sheet_size+'00%'; + if (self.supports_css) { + if (self.use_sheet && px != null && py != null){ + var mul = 100 / (self.sheet_size - 1); + var style = 'background: url('+self.img_sets[img_set].sheet+');background-position:'+(mul*px)+'% '+(mul*py)+'%;background-size:'+self.sheet_size+'00%'; return ''+text+''+extra; - }else if (emoji.use_css_imgs){ + }else if (self.use_css_imgs){ return ''+text+''+extra; }else{ return ''+text+''+extra; @@ -260,52 +350,55 @@ function emoji(){} // Initializes the text emoticon data /** @private */ - emoji.init_emoticons = function(){ - if (emoji.inits.emoticons) return; - emoji.init_colons(); // we require this for the emoticons map - emoji.inits.emoticons = 1; + emoji.prototype.init_emoticons = function(){ + var self = this; + if (self.inits.emoticons) return; + self.init_colons(); // we require this for the emoticons map + self.inits.emoticons = 1; var a = []; - emoji.map.emoticons = {}; - for (var i in emoji.emoticons_data){ + self.map.emoticons = {}; + for (var i in self.emoticons_data){ // because we never see some characters in our text except as entities, we must do some replacing var emoticon = i.replace(/\&/g, '&').replace(/\/g, '>'); - if (!emoji.map.colons[emoji.emoticons_data[i]]) continue; + if (!self.map.colons[self.emoticons_data[i]]) continue; - emoji.map.emoticons[emoticon] = emoji.map.colons[emoji.emoticons_data[i]]; - a.push(emoji.escape_rx(emoticon)); + self.map.emoticons[emoticon] = self.map.colons[self.emoticons_data[i]]; + a.push(self.escape_rx(emoticon)); } - emoji.rx_emoticons = new RegExp(('(^|\\s)('+a.join('|')+')(?=$|[\\s|\\?\\.,!])'), 'g'); + self.rx_emoticons = new RegExp(('(^|\\s)('+a.join('|')+')(?=$|[\\s|\\?\\.,!])'), 'g'); }; // Initializes the colon string data /** @private */ - emoji.init_colons = function(){ - if (emoji.inits.colons) return; - emoji.inits.colons = 1; - emoji.rx_colons = new RegExp('\:[a-zA-Z0-9-_+]+\:(\:skin-tone-[2-6]\:)?', 'g'); - emoji.map.colons = {}; - for (var i in emoji.data){ - for (var j=0; j