diff --git a/app/Events/NewTweetEvent.php b/app/Events/NewTweetEvent.php
index 7a85a3e..5104976 100644
--- a/app/Events/NewTweetEvent.php
+++ b/app/Events/NewTweetEvent.php
@@ -36,13 +36,13 @@ class NewTweetEvent implements ShouldBroadcast
{
$this->tweet_id = $tweet->id;
$this->tweet_date = strtotime($tweet->tweet_date);
- $this->team_name = $tweet->team->name;
- $this->team_color = $tweet->team->color;
- $this->player_username = $tweet->player->twitter;
- $this->player_photo = $tweet->player->photo;
+ $this->team_name = ($tweet->team ? $tweet->team->name : null);
+ $this->team_color = ($tweet->team ? $tweet->team->color : null);
+ $this->player_username = ($tweet->player ? $tweet->player->twitter : null);
+ $this->player_photo = ($tweet->player ? $tweet->player->photo : null);
$this->text = $tweet->text;
$this->photos = json_decode($tweet->photo);
- $this->mission = $tweet->mission->hashtag;
+ $this->mission = ($tweet->mission ? $tweet->mission->hashtag : null);
$this->mission_id = $tweet->mission_id;
}
diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php
index b7fe92d..1a9d859 100644
--- a/app/Http/Controllers/DashboardController.php
+++ b/app/Http/Controllers/DashboardController.php
@@ -117,4 +117,22 @@ class DashboardController extends Controller
'transit_lines' => $transit_lines
]);
}
+
+ public function reply_to_tweet(Request $request) {
+ $tweet = Tweet::where('id', $request->input('tweet_id'))->first();
+ if($tweet) {
+
+ $params = [
+ 'auto_populate_reply_metadata' => true,
+ 'in_reply_to_status_id' => $tweet->tweet_id,
+ 'status' => $request->input('text')
+ ];
+ $response = Twitter::postTweet($params);
+ if($response && $response->id_str) {
+ return response()->json(['result'=>'ok']);
+ }
+
+ }
+ return response()->json(['result'=>'error']);
+ }
}
diff --git a/app/Http/Controllers/TwitterController.php b/app/Http/Controllers/TwitterController.php
index f28c1d5..2fb19fc 100644
--- a/app/Http/Controllers/TwitterController.php
+++ b/app/Http/Controllers/TwitterController.php
@@ -59,7 +59,9 @@ class TwitterController extends BaseController
$tweet->tweet_date = date('Y-m-d H:i:s', strtotime($data['created_at']));
$tweet->save();
- event(new NewTweetEvent($tweet));
+ if($tweet->mission_id) {
+ event(new NewTweetEvent($tweet));
+ }
return $data['id_str'];
}
diff --git a/package.json b/package.json
index 8805092..34a0cbe 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"dependencies": {
"featherlight": "^1.7.6",
"laravel-echo": "^1.3.0",
- "pusher-js": "^4.1.0"
+ "pusher-js": "^4.1.0",
+ "twitter-text": "^1.14.7"
}
}
diff --git a/public/css/app.css b/public/css/app.css
index a565206..63e67bc 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -8629,12 +8629,27 @@ button.close {
background-color: #f5f8fa;
}
-.scorecard .tweet-reply button {
- float: right;
+.scorecard .tweet-reply .bottom {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
}
-.scorecard .tweet-reply .clear {
- clear: both;
+.scorecard .tweet-reply .bottom .fill {
+ -webkit-box-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+}
+
+.scorecard .tweet-reply .bottom img {
+ margin-right: 4px;
+}
+
+.scorecard .tweet-reply .bottom .remaining-chars {
+ margin-right: 10px;
}
.scorecard .tweet-actions {
diff --git a/public/js/app.js b/public/js/app.js
index cbde40d..a533186 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -981,6 +981,8 @@ window.Echo = new __WEBPACK_IMPORTED_MODULE_0_laravel_echo___default.a({
encrypted: true
});
+window.twitter = __webpack_require__(65);
+
/***/ }),
/* 12 */
/***/ (function(module, exports, __webpack_require__) {
@@ -46775,8 +46777,10 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
})])
})) : _vm._e(), _vm._v(" "), _c('div', {
staticClass: "multi-photo-clear"
- })]) : _vm._e()]), _vm._v(" "), _c('div', {
+ })]) : _vm._e()]), _vm._v(" "), (_vm.showReplyBox) ? _c('div', {
staticClass: "tweet-reply"
+ }, [_c('div', {
+ staticClass: "top"
}, [_c('textarea', {
directives: [{
name: "model",
@@ -46789,7 +46793,8 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
"margin-bottom": "4px"
},
attrs: {
- "rows": "2"
+ "rows": "2",
+ "disabled": _vm.replyInProgress
},
domProps: {
"value": (_vm.replyText)
@@ -46800,15 +46805,26 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
_vm.replyText = $event.target.value
}
}
- }), _vm._v(" "), _c('button', {
+ })]), _vm._v(" "), _c('div', {
+ staticClass: "bottom"
+ }, [_c('div', {
+ staticClass: "fill"
+ }), _vm._v(" "), _c('img', {
+ attrs: {
+ "src": "/twitter.ico",
+ "width": "16"
+ }
+ }), _vm._v(" "), _c('div', {
+ staticClass: "remaining-chars"
+ }, [_vm._v(_vm._s(_vm.replyTextCharsRemaining))]), _vm._v(" "), _c('button', {
staticClass: "btn btn-primary",
attrs: {
- "type": "submit",
"disabled": _vm.isTweetReplyDisabled
+ },
+ on: {
+ "click": _vm.sendReply
}
- }, [_vm._v("Reply")]), _vm._v(" "), _c('div', {
- staticClass: "clear"
- })]), _vm._v(" "), _c('div', {
+ }, [_vm._v("Reply")])])]) : _vm._e(), _vm._v(" "), _c('div', {
staticClass: "tweet-actions"
}, [_c('button', {
staticClass: "btn btn-success",
@@ -47073,6 +47089,9 @@ if (false) {
/* 61 */
/***/ (function(module, exports) {
+//
+//
+//
//
//
//
@@ -47244,6 +47263,9 @@ module.exports = {
centers: [],
lines: [],
replyText: '',
+ replyTextCharsRemaining: 140,
+ replyInProgress: false,
+ showReplyBox: true,
selectedDocument: null,
selectedTransitCenter: null,
selectedTransitLine: null,
@@ -47279,7 +47301,7 @@ module.exports = {
}
},
isTweetReplyDisabled: function isTweetReplyDisabled() {
- return this.replyText == '';
+ return this.replyText == '' || this.replyInProgress || this.replyTextCharsRemaining < 0;
}
},
methods: {
@@ -47292,6 +47314,7 @@ module.exports = {
this.selectedM5Singing = false;
this.selectedM5Tipping = false;
this.replyText = '';
+ this.showReplyBox = true;
},
dismiss: function dismiss() {
this.clearState();
@@ -47344,6 +47367,21 @@ module.exports = {
this.clearState();
this.$emit('complete');
}.bind(this));
+ },
+ sendReply: function sendReply() {
+ this.replyInProgress = true;
+
+ $.post("/dashboard/reply", {
+ tweet_id: this.tweet.tweet_id,
+ text: this.replyText
+ }, function (response) {
+ if (response.result == "ok") {
+ this.replyText = '';
+ }
+ this.replyInProgress = false;
+ }.bind(this));
+
+ // setTimeout(function(){ this.replyInProgress = false; this.showReplyBox = false; }.bind(this), 1000);
}
},
watch: {
@@ -47355,6 +47393,9 @@ module.exports = {
$(".multi-photo .photo").featherlight();
}.bind(this));
}
+ },
+ replyText: function replyText(val) {
+ this.replyTextCharsRemaining = 140 - twitter.getTweetLength(val);
}
},
created: function created() {
@@ -47381,5 +47422,1329 @@ module.exports = {
**/
!function(a){"use strict";function b(a,c){if(!(this instanceof b)){var d=new b(a,c);return d.open(),d}this.id=b.id++,this.setup(a,c),this.chainCallbacks(b._callbackChain)}function c(a,b){var c={};for(var d in a)d in b&&(c[d]=a[d],delete a[d]);return c}function d(a,b){var c={},d=new RegExp("^"+b+"([A-Z])(.*)");for(var e in a){var f=e.match(d);if(f){var g=(f[1]+f[2].replace(/([A-Z])/g,"-$1")).toLowerCase();c[g]=a[e]}}return c}if("undefined"==typeof a)return void("console"in window&&window.console.info("Too much lightness, Featherlight needs jQuery."));var e=[],f=function(b){return e=a.grep(e,function(a){return a!==b&&a.$instance.closest("body").length>0})},g={allowfullscreen:1,frameborder:1,height:1,longdesc:1,marginheight:1,marginwidth:1,name:1,referrerpolicy:1,scrolling:1,sandbox:1,src:1,srcdoc:1,width:1},h={keyup:"onKeyUp",resize:"onResize"},i=function(c){a.each(b.opened().reverse(),function(){return c.isDefaultPrevented()||!1!==this[h[c.type]](c)?void 0:(c.preventDefault(),c.stopPropagation(),!1)})},j=function(c){if(c!==b._globalHandlerInstalled){b._globalHandlerInstalled=c;var d=a.map(h,function(a,c){return c+"."+b.prototype.namespace}).join(" ");a(window)[c?"on":"off"](d,i)}};b.prototype={constructor:b,namespace:"featherlight",targetAttr:"data-featherlight",variant:null,resetCss:!1,background:null,openTrigger:"click",closeTrigger:"click",filter:null,root:"body",openSpeed:250,closeSpeed:250,closeOnClick:"background",closeOnEsc:!0,closeIcon:"✕",loading:"",persist:!1,otherClose:null,beforeOpen:a.noop,beforeContent:a.noop,beforeClose:a.noop,afterOpen:a.noop,afterContent:a.noop,afterClose:a.noop,onKeyUp:a.noop,onResize:a.noop,type:null,contentFilters:["jquery","image","html","ajax","iframe","text"],setup:function(b,c){"object"!=typeof b||b instanceof a!=!1||c||(c=b,b=void 0);var d=a.extend(this,c,{target:b}),e=d.resetCss?d.namespace+"-reset":d.namespace,f=a(d.background||['
",{text:b})}}},functionAttributes:["beforeOpen","afterOpen","beforeContent","afterContent","beforeClose","afterClose"],readElementConfig:function(b,c){var d=this,e=new RegExp("^data-"+c+"-(.*)"),f={};return b&&b.attributes&&a.each(b.attributes,function(){var b=this.name.match(e);if(b){var c=this.value,g=a.camelCase(b[1]);if(a.inArray(g,d.functionAttributes)>=0)c=new Function(c);else try{c=JSON.parse(c)}catch(h){}f[g]=c}}),f},extend:function(b,c){var d=function(){this.constructor=b};return d.prototype=this.prototype,b.prototype=new d,b.__super__=this.prototype,a.extend(b,this,c),b.defaults=b.prototype,b},attach:function(b,c,d){var e=this;"object"!=typeof c||c instanceof a!=!1||d||(d=c,c=void 0),d=a.extend({},d);var f,g=d.namespace||e.defaults.namespace,h=a.extend({},e.defaults,e.readElementConfig(b[0],g),d),i=function(g){var i=a(g.currentTarget),j=a.extend({$source:b,$currentTarget:i},e.readElementConfig(b[0],h.namespace),e.readElementConfig(g.currentTarget,h.namespace),d),k=f||i.data("featherlight-persisted")||new e(c,j);"shared"===k.persist?f=k:k.persist!==!1&&i.data("featherlight-persisted",k),j.$currentTarget.blur&&j.$currentTarget.blur(),k.open(g)};return b.on(h.openTrigger+"."+h.namespace,h.filter,i),i},current:function(){var a=this.opened();return a[a.length-1]||null},opened:function(){var b=this;return f(),a.grep(e,function(a){return a instanceof b})},close:function(a){var b=this.current();return b?b.close(a):void 0},_onReady:function(){var b=this;b.autoBind&&(a(b.autoBind).each(function(){b.attach(a(this))}),a(document).on("click",b.autoBind,function(c){if(!c.isDefaultPrevented()){var d=b.attach(a(c.currentTarget));d(c)}}))},_callbackChain:{onKeyUp:function(b,c){return 27===c.keyCode?(this.closeOnEsc&&a.featherlight.close(c),!1):b(c)},beforeOpen:function(b,c){return this._previouslyActive=document.activeElement,this._$previouslyTabbable=a("a, input, select, textarea, iframe, button, iframe, [contentEditable=true]").not("[tabindex]").not(this.$instance.find("button")),this._$previouslyWithTabIndex=a("[tabindex]").not('[tabindex="-1"]'),this._previousWithTabIndices=this._$previouslyWithTabIndex.map(function(b,c){return a(c).attr("tabindex")}),this._$previouslyWithTabIndex.add(this._$previouslyTabbable).attr("tabindex",-1),document.activeElement.blur&&document.activeElement.blur(),b(c)},afterClose:function(b,c){var d=b(c),e=this;return this._$previouslyTabbable.removeAttr("tabindex"),this._$previouslyWithTabIndex.each(function(b,c){a(c).attr("tabindex",e._previousWithTabIndices[b])}),this._previouslyActive.focus(),d},onResize:function(a,b){return this.resize(this.$content.naturalWidth,this.$content.naturalHeight),a(b)},afterContent:function(a,b){var c=a(b);return this.$instance.find("[autofocus]:not([disabled])").focus(),this.onResize(b),c}}}),a.featherlight=b,a.fn.featherlight=function(a,c){return b.attach(this,a,c),this},a(document).ready(function(){b._onReady()})}(jQuery);
+/***/ }),
+/* 65 */
+/***/ (function(module, exports, __webpack_require__) {
+
+var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function() {
+ if (typeof twttr === "undefined" || twttr === null) {
+ var twttr = {};
+ }
+
+ twttr.txt = {};
+ twttr.txt.regexen = {};
+
+ var HTML_ENTITIES = {
+ '&': '&',
+ '>': '>',
+ '<': '<',
+ '"': '"',
+ "'": '''
+ };
+
+ // HTML escaping
+ twttr.txt.htmlEscape = function(text) {
+ return text && text.replace(/[&"'><]/g, function(character) {
+ return HTML_ENTITIES[character];
+ });
+ };
+
+ // Builds a RegExp
+ function regexSupplant(regex, flags) {
+ flags = flags || "";
+ if (typeof regex !== "string") {
+ if (regex.global && flags.indexOf("g") < 0) {
+ flags += "g";
+ }
+ if (regex.ignoreCase && flags.indexOf("i") < 0) {
+ flags += "i";
+ }
+ if (regex.multiline && flags.indexOf("m") < 0) {
+ flags += "m";
+ }
+
+ regex = regex.source;
+ }
+
+ return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
+ var newRegex = twttr.txt.regexen[name] || "";
+ if (typeof newRegex !== "string") {
+ newRegex = newRegex.source;
+ }
+ return newRegex;
+ }), flags);
+ }
+
+ twttr.txt.regexSupplant = regexSupplant;
+
+ // simple string interpolation
+ function stringSupplant(str, values) {
+ return str.replace(/#\{(\w+)\}/g, function(match, name) {
+ return values[name] || "";
+ });
+ }
+
+ twttr.txt.stringSupplant = stringSupplant;
+
+ twttr.txt.regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/;
+ twttr.txt.regexen.spaces = regexSupplant(/[#{spaces_group}]/);
+ twttr.txt.regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/;
+ twttr.txt.regexen.invalid_chars = regexSupplant(/[#{invalid_chars_group}]/);
+ twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/;
+ twttr.txt.regexen.rtl_chars = /[\u0600-\u06FF]|[\u0750-\u077F]|[\u0590-\u05FF]|[\uFE70-\uFEFF]/mg;
+ twttr.txt.regexen.non_bmp_code_pairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/mg;
+
+ twttr.txt.regexen.latinAccentChars = /\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u024F\u0253\u0254\u0256\u0257\u0259\u025B\u0263\u0268\u026F\u0272\u0289\u028B\u02BB\u0300-\u036F\u1E00-\u1EFF/;
+
+ // Generated from unicode_regex/unicode_regex_groups.scala, same as objective c's \p{L}\p{M}
+ twttr.txt.regexen.bmpLetterAndMarks = /A-Za-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0300-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u0483-\u052f\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u05d0-\u05ea\u05f0-\u05f2\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06df-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u06ff\u0710-\u074a\u074d-\u07b1\u07ca-\u07f5\u07fa\u0800-\u082d\u0840-\u085b\u08a0-\u08b2\u08e4-\u0963\u0971-\u0983\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bc-\u09c4\u09c7\u09c8\u09cb-\u09ce\u09d7\u09dc\u09dd\u09df-\u09e3\u09f0\u09f1\u0a01-\u0a03\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a59-\u0a5c\u0a5e\u0a70-\u0a75\u0a81-\u0a83\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abc-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ad0\u0ae0-\u0ae3\u0b01-\u0b03\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3c-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5c\u0b5d\u0b5f-\u0b63\u0b71\u0b82\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd0\u0bd7\u0c00-\u0c03\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c58\u0c59\u0c60-\u0c63\u0c81-\u0c83\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbc-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0cde\u0ce0-\u0ce3\u0cf1\u0cf2\u0d01-\u0d03\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d-\u0d44\u0d46-\u0d48\u0d4a-\u0d4e\u0d57\u0d60-\u0d63\u0d7a-\u0d7f\u0d82\u0d83\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e01-\u0e3a\u0e40-\u0e4e\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb9\u0ebb-\u0ebd\u0ec0-\u0ec4\u0ec6\u0ec8-\u0ecd\u0edc-\u0edf\u0f00\u0f18\u0f19\u0f35\u0f37\u0f39\u0f3e-\u0f47\u0f49-\u0f6c\u0f71-\u0f84\u0f86-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u103f\u1050-\u108f\u109a-\u109d\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u135d-\u135f\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16f1-\u16f8\u1700-\u170c\u170e-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176c\u176e-\u1770\u1772\u1773\u1780-\u17d3\u17d7\u17dc\u17dd\u180b-\u180d\u1820-\u1877\u1880-\u18aa\u18b0-\u18f5\u1900-\u191e\u1920-\u192b\u1930-\u193b\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a1b\u1a20-\u1a5e\u1a60-\u1a7c\u1a7f\u1aa7\u1ab0-\u1abe\u1b00-\u1b4b\u1b6b-\u1b73\u1b80-\u1baf\u1bba-\u1bf3\u1c00-\u1c37\u1c4d-\u1c4f\u1c5a-\u1c7d\u1cd0-\u1cd2\u1cd4-\u1cf6\u1cf8\u1cf9\u1d00-\u1df5\u1dfc-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u20d0-\u20f0\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d7f-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2de0-\u2dff\u2e2f\u3005\u3006\u302a-\u302f\u3031-\u3035\u303b\u303c\u3041-\u3096\u3099\u309a\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua672\ua674-\ua67d\ua67f-\ua69d\ua69f-\ua6e5\ua6f0\ua6f1\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua7ad\ua7b0\ua7b1\ua7f7-\ua827\ua840-\ua873\ua880-\ua8c4\ua8e0-\ua8f7\ua8fb\ua90a-\ua92d\ua930-\ua953\ua960-\ua97c\ua980-\ua9c0\ua9cf\ua9e0-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa36\uaa40-\uaa4d\uaa60-\uaa76\uaa7a-\uaac2\uaadb-\uaadd\uaae0-\uaaef\uaaf2-\uaaf6\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab5f\uab64\uab65\uabc0-\uabea\uabec\uabed\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf870-\uf87f\uf882\uf884-\uf89f\uf8b8\uf8c1-\uf8d6\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe00-\ufe0f\ufe20-\ufe2d\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc/;
+ twttr.txt.regexen.astralLetterAndMarks = /\ud800[\udc00-\udc0b\udc0d-\udc26\udc28-\udc3a\udc3c\udc3d\udc3f-\udc4d\udc50-\udc5d\udc80-\udcfa\uddfd\ude80-\ude9c\udea0-\uded0\udee0\udf00-\udf1f\udf30-\udf40\udf42-\udf49\udf50-\udf7a\udf80-\udf9d\udfa0-\udfc3\udfc8-\udfcf]|\ud801[\udc00-\udc9d\udd00-\udd27\udd30-\udd63\ude00-\udf36\udf40-\udf55\udf60-\udf67]|\ud802[\udc00-\udc05\udc08\udc0a-\udc35\udc37\udc38\udc3c\udc3f-\udc55\udc60-\udc76\udc80-\udc9e\udd00-\udd15\udd20-\udd39\udd80-\uddb7\uddbe\uddbf\ude00-\ude03\ude05\ude06\ude0c-\ude13\ude15-\ude17\ude19-\ude33\ude38-\ude3a\ude3f\ude60-\ude7c\ude80-\ude9c\udec0-\udec7\udec9-\udee6\udf00-\udf35\udf40-\udf55\udf60-\udf72\udf80-\udf91]|\ud803[\udc00-\udc48]|\ud804[\udc00-\udc46\udc7f-\udcba\udcd0-\udce8\udd00-\udd34\udd50-\udd73\udd76\udd80-\uddc4\uddda\ude00-\ude11\ude13-\ude37\udeb0-\udeea\udf01-\udf03\udf05-\udf0c\udf0f\udf10\udf13-\udf28\udf2a-\udf30\udf32\udf33\udf35-\udf39\udf3c-\udf44\udf47\udf48\udf4b-\udf4d\udf57\udf5d-\udf63\udf66-\udf6c\udf70-\udf74]|\ud805[\udc80-\udcc5\udcc7\udd80-\uddb5\uddb8-\uddc0\ude00-\ude40\ude44\ude80-\udeb7]|\ud806[\udca0-\udcdf\udcff\udec0-\udef8]|\ud808[\udc00-\udf98]|\ud80c[\udc00-\udfff]|\ud80d[\udc00-\udc2e]|\ud81a[\udc00-\ude38\ude40-\ude5e\uded0-\udeed\udef0-\udef4\udf00-\udf36\udf40-\udf43\udf63-\udf77\udf7d-\udf8f]|\ud81b[\udf00-\udf44\udf50-\udf7e\udf8f-\udf9f]|\ud82c[\udc00\udc01]|\ud82f[\udc00-\udc6a\udc70-\udc7c\udc80-\udc88\udc90-\udc99\udc9d\udc9e]|\ud834[\udd65-\udd69\udd6d-\udd72\udd7b-\udd82\udd85-\udd8b\uddaa-\uddad\ude42-\ude44]|\ud835[\udc00-\udc54\udc56-\udc9c\udc9e\udc9f\udca2\udca5\udca6\udca9-\udcac\udcae-\udcb9\udcbb\udcbd-\udcc3\udcc5-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd1e-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd52-\udea5\udea8-\udec0\udec2-\udeda\udedc-\udefa\udefc-\udf14\udf16-\udf34\udf36-\udf4e\udf50-\udf6e\udf70-\udf88\udf8a-\udfa8\udfaa-\udfc2\udfc4-\udfcb]|\ud83a[\udc00-\udcc4\udcd0-\udcd6]|\ud83b[\ude00-\ude03\ude05-\ude1f\ude21\ude22\ude24\ude27\ude29-\ude32\ude34-\ude37\ude39\ude3b\ude42\ude47\ude49\ude4b\ude4d-\ude4f\ude51\ude52\ude54\ude57\ude59\ude5b\ude5d\ude5f\ude61\ude62\ude64\ude67-\ude6a\ude6c-\ude72\ude74-\ude77\ude79-\ude7c\ude7e\ude80-\ude89\ude8b-\ude9b\udea1-\udea3\udea5-\udea9\udeab-\udebb]|\ud840[\udc00-\udfff]|\ud841[\udc00-\udfff]|\ud842[\udc00-\udfff]|\ud843[\udc00-\udfff]|\ud844[\udc00-\udfff]|\ud845[\udc00-\udfff]|\ud846[\udc00-\udfff]|\ud847[\udc00-\udfff]|\ud848[\udc00-\udfff]|\ud849[\udc00-\udfff]|\ud84a[\udc00-\udfff]|\ud84b[\udc00-\udfff]|\ud84c[\udc00-\udfff]|\ud84d[\udc00-\udfff]|\ud84e[\udc00-\udfff]|\ud84f[\udc00-\udfff]|\ud850[\udc00-\udfff]|\ud851[\udc00-\udfff]|\ud852[\udc00-\udfff]|\ud853[\udc00-\udfff]|\ud854[\udc00-\udfff]|\ud855[\udc00-\udfff]|\ud856[\udc00-\udfff]|\ud857[\udc00-\udfff]|\ud858[\udc00-\udfff]|\ud859[\udc00-\udfff]|\ud85a[\udc00-\udfff]|\ud85b[\udc00-\udfff]|\ud85c[\udc00-\udfff]|\ud85d[\udc00-\udfff]|\ud85e[\udc00-\udfff]|\ud85f[\udc00-\udfff]|\ud860[\udc00-\udfff]|\ud861[\udc00-\udfff]|\ud862[\udc00-\udfff]|\ud863[\udc00-\udfff]|\ud864[\udc00-\udfff]|\ud865[\udc00-\udfff]|\ud866[\udc00-\udfff]|\ud867[\udc00-\udfff]|\ud868[\udc00-\udfff]|\ud869[\udc00-\uded6\udf00-\udfff]|\ud86a[\udc00-\udfff]|\ud86b[\udc00-\udfff]|\ud86c[\udc00-\udfff]|\ud86d[\udc00-\udf34\udf40-\udfff]|\ud86e[\udc00-\udc1d]|\ud87e[\udc00-\ude1d]|\udb40[\udd00-\uddef]/;
+
+ // Generated from unicode_regex/unicode_regex_groups.scala, same as objective c's \p{Nd}
+ twttr.txt.regexen.bmpNumerals = /0-9\u0660-\u0669\u06f0-\u06f9\u07c0-\u07c9\u0966-\u096f\u09e6-\u09ef\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be6-\u0bef\u0c66-\u0c6f\u0ce6-\u0cef\u0d66-\u0d6f\u0de6-\u0def\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29\u1040-\u1049\u1090-\u1099\u17e0-\u17e9\u1810-\u1819\u1946-\u194f\u19d0-\u19d9\u1a80-\u1a89\u1a90-\u1a99\u1b50-\u1b59\u1bb0-\u1bb9\u1c40-\u1c49\u1c50-\u1c59\ua620-\ua629\ua8d0-\ua8d9\ua900-\ua909\ua9d0-\ua9d9\ua9f0-\ua9f9\uaa50-\uaa59\uabf0-\uabf9\uff10-\uff19/;
+ twttr.txt.regexen.astralNumerals = /\ud801[\udca0-\udca9]|\ud804[\udc66-\udc6f\udcf0-\udcf9\udd36-\udd3f\uddd0-\uddd9\udef0-\udef9]|\ud805[\udcd0-\udcd9\ude50-\ude59\udec0-\udec9]|\ud806[\udce0-\udce9]|\ud81a[\ude60-\ude69\udf50-\udf59]|\ud835[\udfce-\udfff]/;
+
+ twttr.txt.regexen.hashtagSpecialChars = /_\u200c\u200d\ua67e\u05be\u05f3\u05f4\uff5e\u301c\u309b\u309c\u30a0\u30fb\u3003\u0f0b\u0f0c\xb7/;
+
+ // A hashtag must contain at least one unicode letter or mark, as well as numbers, underscores, and select special characters.
+ twttr.txt.regexen.hashSigns = /[##]/;
+ twttr.txt.regexen.hashtagAlpha = regexSupplant(/(?:[#{bmpLetterAndMarks}]|(?=#{non_bmp_code_pairs})(?:#{astralLetterAndMarks}))/);
+ twttr.txt.regexen.hashtagAlphaNumeric = regexSupplant(/(?:[#{bmpLetterAndMarks}#{bmpNumerals}#{hashtagSpecialChars}]|(?=#{non_bmp_code_pairs})(?:#{astralLetterAndMarks}|#{astralNumerals}))/);
+ twttr.txt.regexen.endHashtagMatch = regexSupplant(/^(?:#{hashSigns}|:\/\/)/);
+ twttr.txt.regexen.codePoint = /(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])/;
+ twttr.txt.regexen.hashtagBoundary = regexSupplant(/(?:^|\uFE0E|\uFE0F|$|(?!#{hashtagAlphaNumeric}|&)#{codePoint})/);
+ twttr.txt.regexen.validHashtag = regexSupplant(/(#{hashtagBoundary})(#{hashSigns})(?!\uFE0F|\u20E3)(#{hashtagAlphaNumeric}*#{hashtagAlpha}#{hashtagAlphaNumeric}*)/gi);
+
+ // Mention related regex collection
+ twttr.txt.regexen.validMentionPrecedingChars = /(?:^|[^a-zA-Z0-9_!#$%&*@@]|(?:^|[^a-zA-Z0-9_+~.-])(?:rt|RT|rT|Rt):?)/;
+ twttr.txt.regexen.atSigns = /[@@]/;
+ twttr.txt.regexen.validMentionOrList = regexSupplant(
+ '(#{validMentionPrecedingChars})' + // $1: Preceding character
+ '(#{atSigns})' + // $2: At mark
+ '([a-zA-Z0-9_]{1,20})' + // $3: Screen name
+ '(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?' // $4: List (optional)
+ , 'g');
+ twttr.txt.regexen.validReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/);
+ twttr.txt.regexen.endMentionMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/);
+
+ // URL related regex collection
+ twttr.txt.regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalid_chars_group}]|^)/);
+ twttr.txt.regexen.invalidUrlWithoutProtocolPrecedingChars = /[-_.\/]$/;
+ twttr.txt.regexen.invalidDomainChars = stringSupplant("#{punct}#{spaces_group}#{invalid_chars_group}", twttr.txt.regexen);
+ twttr.txt.regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/);
+ twttr.txt.regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
+ twttr.txt.regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
+ twttr.txt.regexen.validGTLD = regexSupplant(RegExp(
+'(?:(?:' +
+ '삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
+ '政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
+ 'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
+ 'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
+ 'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
+ 'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
+ 'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
+ 'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
+ 'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
+ 'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
+ 'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
+ 'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
+ 'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
+ 'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
+ 'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
+ 'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
+ 'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
+ 'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
+ 'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
+ 'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
+ 'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
+ 'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
+ 'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
+ 'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
+ 'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
+ 'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
+ 'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
+ 'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
+ 'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
+ 'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
+ 'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
+ 'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
+ 'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
+ 'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
+ 'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
+ 'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
+ 'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
+ 'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
+ 'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
+ 'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
+ 'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
+ 'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
+ 'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
+ 'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
+ 'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
+ 'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
+ 'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
+ 'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
+ 'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
+ 'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
+ 'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
+ 'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
+ 'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
+ 'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
+ 'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
+ 'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
+ 'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
+ 'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
+ 'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
+ 'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
+ 'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
+ 'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
+ 'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
+ 'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
+ 'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
+ 'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
+ 'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
+ 'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
+ 'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
+ 'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
+ 'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
+ 'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
+ 'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
+ 'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
+ 'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
+ 'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
+ 'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
+ 'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
+ 'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
+ 'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
+ 'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
+ 'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
+ 'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
+ 'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
+ 'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
+ 'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
+ 'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
+ 'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
+')(?=[^0-9a-zA-Z@]|$))'));
+ twttr.txt.regexen.validCCTLD = regexSupplant(RegExp(
+'(?:(?:' +
+ '한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
+ 'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
+ 'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
+ 'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' +
+ 'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' +
+ 're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' +
+ 'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' +
+ 'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' +
+ 'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
+ 'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
+ 'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
+')(?=[^0-9a-zA-Z@]|$))'));
+ twttr.txt.regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
+ twttr.txt.regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
+ twttr.txt.regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
+ twttr.txt.regexen.validAsciiDomain = regexSupplant(/(?:(?:[\-a-z0-9#{latinAccentChars}]+)\.)+(?:#{validGTLD}|#{validCCTLD}|#{validPunycode})/gi);
+ twttr.txt.regexen.invalidShortDomain = regexSupplant(/^#{validDomainName}#{validCCTLD}$/i);
+ twttr.txt.regexen.validSpecialShortDomain = regexSupplant(/^#{validDomainName}#{validSpecialCCTLD}$/i);
+ twttr.txt.regexen.validPortNumber = /[0-9]+/;
+ twttr.txt.regexen.cyrillicLettersAndMarks = /\u0400-\u04FF/;
+ twttr.txt.regexen.validGeneralUrlPathChars = regexSupplant(/[a-z#{cyrillicLettersAndMarks}0-9!\*';:=\+,\.\$\/%#\[\]\-_~@\|{latinAccentChars}]/i);
+ // Allow URL paths to contain up to two nested levels of balanced parens
+ // 1. Used in Wikipedia URLs like /Primer_(film)
+ // 2. Used in IIS sessions like /S(dfd346)/
+ // 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/
+ twttr.txt.regexen.validUrlBalancedParens = regexSupplant(
+ '\\(' +
+ '(?:' +
+ '#{validGeneralUrlPathChars}+' +
+ '|' +
+ // allow one nested level of balanced parentheses
+ '(?:' +
+ '#{validGeneralUrlPathChars}*' +
+ '\\(' +
+ '#{validGeneralUrlPathChars}+' +
+ '\\)' +
+ '#{validGeneralUrlPathChars}*' +
+ ')' +
+ ')' +
+ '\\)'
+ , 'i');
+ // Valid end-of-path chracters (so /foo. does not gobble the period).
+ // 1. Allow = for empty URL parameters and other URL-join artifacts
+ twttr.txt.regexen.validUrlPathEndingChars = regexSupplant(/[\+\-a-z#{cyrillicLettersAndMarks}0-9=_#\/#{latinAccentChars}]|(?:#{validUrlBalancedParens})/i);
+ // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/
+ twttr.txt.regexen.validUrlPath = regexSupplant('(?:' +
+ '(?:' +
+ '#{validGeneralUrlPathChars}*' +
+ '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
+ '#{validUrlPathEndingChars}'+
+ ')|(?:@#{validGeneralUrlPathChars}+\/)'+
+ ')', 'i');
+
+ twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
+ twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
+ twttr.txt.regexen.extractUrl = regexSupplant(
+ '(' + // $1 total match
+ '(#{validUrlPrecedingChars})' + // $2 Preceeding chracter
+ '(' + // $3 URL
+ '(https?:\\/\\/)?' + // $4 Protocol (optional)
+ '(#{validDomain})' + // $5 Domain(s)
+ '(?::(#{validPortNumber}))?' + // $6 Port number (optional)
+ '(\\/#{validUrlPath}*)?' + // $7 URL Path
+ '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $8 Query String
+ ')' +
+ ')'
+ , 'gi');
+
+ twttr.txt.regexen.validTcoUrl = /^https?:\/\/t\.co\/[a-z0-9]+/i;
+ twttr.txt.regexen.urlHasProtocol = /^https?:\/\//i;
+ twttr.txt.regexen.urlHasHttps = /^https:\/\//i;
+
+ // cashtag related regex
+ twttr.txt.regexen.cashtag = /[a-z]{1,6}(?:[._][a-z]{1,2})?/i;
+ twttr.txt.regexen.validCashtag = regexSupplant('(^|#{spaces})(\\$)(#{cashtag})(?=$|\\s|[#{punct}])', 'gi');
+
+ // These URL validation pattern strings are based on the ABNF from RFC 3986
+ twttr.txt.regexen.validateUrlUnreserved = /[a-z\u0400-\u04FF0-9\-._~]/i;
+ twttr.txt.regexen.validateUrlPctEncoded = /(?:%[0-9a-f]{2})/i;
+ twttr.txt.regexen.validateUrlSubDelims = /[!$&'()*+,;=]/i;
+ twttr.txt.regexen.validateUrlPchar = regexSupplant('(?:' +
+ '#{validateUrlUnreserved}|' +
+ '#{validateUrlPctEncoded}|' +
+ '#{validateUrlSubDelims}|' +
+ '[:|@]' +
+ ')', 'i');
+
+ twttr.txt.regexen.validateUrlScheme = /(?:[a-z][a-z0-9+\-.]*)/i;
+ twttr.txt.regexen.validateUrlUserinfo = regexSupplant('(?:' +
+ '#{validateUrlUnreserved}|' +
+ '#{validateUrlPctEncoded}|' +
+ '#{validateUrlSubDelims}|' +
+ ':' +
+ ')*', 'i');
+
+ twttr.txt.regexen.validateUrlDecOctet = /(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9]{2})|(?:2[0-4][0-9])|(?:25[0-5]))/i;
+ twttr.txt.regexen.validateUrlIpv4 = regexSupplant(/(?:#{validateUrlDecOctet}(?:\.#{validateUrlDecOctet}){3})/i);
+
+ // Punting on real IPv6 validation for now
+ twttr.txt.regexen.validateUrlIpv6 = /(?:\[[a-f0-9:\.]+\])/i;
+
+ // Also punting on IPvFuture for now
+ twttr.txt.regexen.validateUrlIp = regexSupplant('(?:' +
+ '#{validateUrlIpv4}|' +
+ '#{validateUrlIpv6}' +
+ ')', 'i');
+
+ // This is more strict than the rfc specifies
+ twttr.txt.regexen.validateUrlSubDomainSegment = /(?:[a-z0-9](?:[a-z0-9_\-]*[a-z0-9])?)/i;
+ twttr.txt.regexen.validateUrlDomainSegment = /(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?)/i;
+ twttr.txt.regexen.validateUrlDomainTld = /(?:[a-z](?:[a-z0-9\-]*[a-z0-9])?)/i;
+ twttr.txt.regexen.validateUrlDomain = regexSupplant(/(?:(?:#{validateUrlSubDomainSegment}\.)*(?:#{validateUrlDomainSegment}\.)#{validateUrlDomainTld})/i);
+
+ twttr.txt.regexen.validateUrlHost = regexSupplant('(?:' +
+ '#{validateUrlIp}|' +
+ '#{validateUrlDomain}' +
+ ')', 'i');
+
+ // Unencoded internationalized domains - this doesn't check for invalid UTF-8 sequences
+ twttr.txt.regexen.validateUrlUnicodeSubDomainSegment = /(?:(?:[a-z0-9]|[^\u0000-\u007f])(?:(?:[a-z0-9_\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i;
+ twttr.txt.regexen.validateUrlUnicodeDomainSegment = /(?:(?:[a-z0-9]|[^\u0000-\u007f])(?:(?:[a-z0-9\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i;
+ twttr.txt.regexen.validateUrlUnicodeDomainTld = /(?:(?:[a-z]|[^\u0000-\u007f])(?:(?:[a-z0-9\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i;
+ twttr.txt.regexen.validateUrlUnicodeDomain = regexSupplant(/(?:(?:#{validateUrlUnicodeSubDomainSegment}\.)*(?:#{validateUrlUnicodeDomainSegment}\.)#{validateUrlUnicodeDomainTld})/i);
+
+ twttr.txt.regexen.validateUrlUnicodeHost = regexSupplant('(?:' +
+ '#{validateUrlIp}|' +
+ '#{validateUrlUnicodeDomain}' +
+ ')', 'i');
+
+ twttr.txt.regexen.validateUrlPort = /[0-9]{1,5}/;
+
+ twttr.txt.regexen.validateUrlUnicodeAuthority = regexSupplant(
+ '(?:(#{validateUrlUserinfo})@)?' + // $1 userinfo
+ '(#{validateUrlUnicodeHost})' + // $2 host
+ '(?::(#{validateUrlPort}))?' //$3 port
+ , "i");
+
+ twttr.txt.regexen.validateUrlAuthority = regexSupplant(
+ '(?:(#{validateUrlUserinfo})@)?' + // $1 userinfo
+ '(#{validateUrlHost})' + // $2 host
+ '(?::(#{validateUrlPort}))?' // $3 port
+ , "i");
+
+ twttr.txt.regexen.validateUrlPath = regexSupplant(/(\/#{validateUrlPchar}*)*/i);
+ twttr.txt.regexen.validateUrlQuery = regexSupplant(/(#{validateUrlPchar}|\/|\?)*/i);
+ twttr.txt.regexen.validateUrlFragment = regexSupplant(/(#{validateUrlPchar}|\/|\?)*/i);
+
+ // Modified version of RFC 3986 Appendix B
+ twttr.txt.regexen.validateUrlUnencoded = regexSupplant(
+ '^' + // Full URL
+ '(?:' +
+ '([^:/?#]+):\\/\\/' + // $1 Scheme
+ ')?' +
+ '([^/?#]*)' + // $2 Authority
+ '([^?#]*)' + // $3 Path
+ '(?:' +
+ '\\?([^#]*)' + // $4 Query
+ ')?' +
+ '(?:' +
+ '#(.*)' + // $5 Fragment
+ ')?$'
+ , "i");
+
+
+ // Default CSS class for auto-linked lists (along with the url class)
+ var DEFAULT_LIST_CLASS = "tweet-url list-slug";
+ // Default CSS class for auto-linked usernames (along with the url class)
+ var DEFAULT_USERNAME_CLASS = "tweet-url username";
+ // Default CSS class for auto-linked hashtags (along with the url class)
+ var DEFAULT_HASHTAG_CLASS = "tweet-url hashtag";
+ // Default CSS class for auto-linked cashtags (along with the url class)
+ var DEFAULT_CASHTAG_CLASS = "tweet-url cashtag";
+ // Options which should not be passed as HTML attributes
+ var OPTIONS_NOT_ATTRIBUTES = {'urlClass':true, 'listClass':true, 'usernameClass':true, 'hashtagClass':true, 'cashtagClass':true,
+ 'usernameUrlBase':true, 'listUrlBase':true, 'hashtagUrlBase':true, 'cashtagUrlBase':true,
+ 'usernameUrlBlock':true, 'listUrlBlock':true, 'hashtagUrlBlock':true, 'linkUrlBlock':true,
+ 'usernameIncludeSymbol':true, 'suppressLists':true, 'suppressNoFollow':true, 'targetBlank':true,
+ 'suppressDataScreenName':true, 'urlEntities':true, 'symbolTag':true, 'textWithSymbolTag':true, 'urlTarget':true,
+ 'invisibleTagAttrs':true, 'linkAttributeBlock':true, 'linkTextBlock': true, 'htmlEscapeNonEntities': true
+ };
+
+ var BOOLEAN_ATTRIBUTES = {'disabled':true, 'readonly':true, 'multiple':true, 'checked':true};
+
+ // Simple object cloning function for simple objects
+ function clone(o) {
+ var r = {};
+ for (var k in o) {
+ if (o.hasOwnProperty(k)) {
+ r[k] = o[k];
+ }
+ }
+
+ return r;
+ }
+
+ twttr.txt.tagAttrs = function(attributes) {
+ var htmlAttrs = "";
+ for (var k in attributes) {
+ var v = attributes[k];
+ if (BOOLEAN_ATTRIBUTES[k]) {
+ v = v ? k : null;
+ }
+ if (v == null) continue;
+ htmlAttrs += " " + twttr.txt.htmlEscape(k) + "=\"" + twttr.txt.htmlEscape(v.toString()) + "\"";
+ }
+ return htmlAttrs;
+ };
+
+ twttr.txt.linkToText = function(entity, text, attributes, options) {
+ if (!options.suppressNoFollow) {
+ attributes.rel = "nofollow";
+ }
+ // if linkAttributeBlock is specified, call it to modify the attributes
+ if (options.linkAttributeBlock) {
+ options.linkAttributeBlock(entity, attributes);
+ }
+ // if linkTextBlock is specified, call it to get a new/modified link text
+ if (options.linkTextBlock) {
+ text = options.linkTextBlock(entity, text);
+ }
+ var d = {
+ text: text,
+ attr: twttr.txt.tagAttrs(attributes)
+ };
+ return stringSupplant("
#{text}", d);
+ };
+
+ twttr.txt.linkToTextWithSymbol = function(entity, symbol, text, attributes, options) {
+ var taggedSymbol = options.symbolTag ? "<" + options.symbolTag + ">" + symbol + ""+ options.symbolTag + ">" : symbol;
+ text = twttr.txt.htmlEscape(text);
+ var taggedText = options.textWithSymbolTag ? "<" + options.textWithSymbolTag + ">" + text + ""+ options.textWithSymbolTag + ">" : text;
+
+ if (options.usernameIncludeSymbol || !symbol.match(twttr.txt.regexen.atSigns)) {
+ return twttr.txt.linkToText(entity, taggedSymbol + taggedText, attributes, options);
+ } else {
+ return taggedSymbol + twttr.txt.linkToText(entity, taggedText, attributes, options);
+ }
+ };
+
+ twttr.txt.linkToHashtag = function(entity, text, options) {
+ var hash = text.substring(entity.indices[0], entity.indices[0] + 1);
+ var hashtag = twttr.txt.htmlEscape(entity.hashtag);
+ var attrs = clone(options.htmlAttrs || {});
+ attrs.href = options.hashtagUrlBase + hashtag;
+ attrs.title = "#" + hashtag;
+ attrs["class"] = options.hashtagClass;
+ if (hashtag.charAt(0).match(twttr.txt.regexen.rtl_chars)){
+ attrs["class"] += " rtl";
+ }
+ if (options.targetBlank) {
+ attrs.target = '_blank';
+ }
+
+ return twttr.txt.linkToTextWithSymbol(entity, hash, hashtag, attrs, options);
+ };
+
+ twttr.txt.linkToCashtag = function(entity, text, options) {
+ var cashtag = twttr.txt.htmlEscape(entity.cashtag);
+ var attrs = clone(options.htmlAttrs || {});
+ attrs.href = options.cashtagUrlBase + cashtag;
+ attrs.title = "$" + cashtag;
+ attrs["class"] = options.cashtagClass;
+ if (options.targetBlank) {
+ attrs.target = '_blank';
+ }
+
+ return twttr.txt.linkToTextWithSymbol(entity, "$", cashtag, attrs, options);
+ };
+
+ twttr.txt.linkToMentionAndList = function(entity, text, options) {
+ var at = text.substring(entity.indices[0], entity.indices[0] + 1);
+ var user = twttr.txt.htmlEscape(entity.screenName);
+ var slashListname = twttr.txt.htmlEscape(entity.listSlug);
+ var isList = entity.listSlug && !options.suppressLists;
+ var attrs = clone(options.htmlAttrs || {});
+ attrs["class"] = (isList ? options.listClass : options.usernameClass);
+ attrs.href = isList ? options.listUrlBase + user + slashListname : options.usernameUrlBase + user;
+ if (!isList && !options.suppressDataScreenName) {
+ attrs['data-screen-name'] = user;
+ }
+ if (options.targetBlank) {
+ attrs.target = '_blank';
+ }
+
+ return twttr.txt.linkToTextWithSymbol(entity, at, isList ? user + slashListname : user, attrs, options);
+ };
+
+ twttr.txt.linkToUrl = function(entity, text, options) {
+ var url = entity.url;
+ var displayUrl = url;
+ var linkText = twttr.txt.htmlEscape(displayUrl);
+
+ // If the caller passed a urlEntities object (provided by a Twitter API
+ // response with include_entities=true), we use that to render the display_url
+ // for each URL instead of it's underlying t.co URL.
+ var urlEntity = (options.urlEntities && options.urlEntities[url]) || entity;
+ if (urlEntity.display_url) {
+ linkText = twttr.txt.linkTextWithEntity(urlEntity, options);
+ }
+
+ var attrs = clone(options.htmlAttrs || {});
+
+ if (!url.match(twttr.txt.regexen.urlHasProtocol)) {
+ url = "http://" + url;
+ }
+ attrs.href = url;
+
+ if (options.targetBlank) {
+ attrs.target = '_blank';
+ }
+
+ // set class only if urlClass is specified.
+ if (options.urlClass) {
+ attrs["class"] = options.urlClass;
+ }
+
+ // set target only if urlTarget is specified.
+ if (options.urlTarget) {
+ attrs.target = options.urlTarget;
+ }
+
+ if (!options.title && urlEntity.display_url) {
+ attrs.title = urlEntity.expanded_url;
+ }
+
+ return twttr.txt.linkToText(entity, linkText, attrs, options);
+ };
+
+ twttr.txt.linkTextWithEntity = function (entity, options) {
+ var displayUrl = entity.display_url;
+ var expandedUrl = entity.expanded_url;
+
+ // Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
+ // should contain the full original URL (expanded_url), not the display URL.
+ //
+ // Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
+ // font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
+ // Elements with font-size:0 get copied even though they are not visible.
+ // Note that display:none doesn't work here. Elements with display:none don't get copied.
+ //
+ // Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
+ // wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
+ // everything with the tco-ellipsis class.
+ //
+ // Exception: pic.twitter.com images, for which expandedUrl = "https://twitter.com/#!/username/status/1234/photo/1
+ // For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
+ // For a pic.twitter.com URL, the only elided part will be the "https://", so this is fine.
+
+ var displayUrlSansEllipses = displayUrl.replace(/…/g, ""); // We have to disregard ellipses for matching
+ // Note: we currently only support eliding parts of the URL at the beginning or the end.
+ // Eventually we may want to elide parts of the URL in the *middle*. If so, this code will
+ // become more complicated. We will probably want to create a regexp out of display URL,
+ // replacing every ellipsis with a ".*".
+ if (expandedUrl.indexOf(displayUrlSansEllipses) != -1) {
+ var displayUrlIndex = expandedUrl.indexOf(displayUrlSansEllipses);
+ var v = {
+ displayUrlSansEllipses: displayUrlSansEllipses,
+ // Portion of expandedUrl that precedes the displayUrl substring
+ beforeDisplayUrl: expandedUrl.substr(0, displayUrlIndex),
+ // Portion of expandedUrl that comes after displayUrl
+ afterDisplayUrl: expandedUrl.substr(displayUrlIndex + displayUrlSansEllipses.length),
+ precedingEllipsis: displayUrl.match(/^…/) ? "…" : "",
+ followingEllipsis: displayUrl.match(/…$/) ? "…" : ""
+ };
+ for (var k in v) {
+ if (v.hasOwnProperty(k)) {
+ v[k] = twttr.txt.htmlEscape(v[k]);
+ }
+ }
+ // As an example: The user tweets "hi http://longdomainname.com/foo"
+ // This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
+ // This will get rendered as:
+ //
+ // …
+ //
+ // http://longdomai
+ //
+ //
+ // nname.com/foo
+ //
+ //
+ //
+ // …
+ //
+ v['invisible'] = options.invisibleTagAttrs;
+ return stringSupplant("
#{precedingEllipsis} #{beforeDisplayUrl}#{displayUrlSansEllipses}#{afterDisplayUrl} #{followingEllipsis}", v);
+ }
+ return displayUrl;
+ };
+
+ twttr.txt.autoLinkEntities = function(text, entities, options) {
+ options = clone(options || {});
+
+ options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS;
+ options.hashtagUrlBase = options.hashtagUrlBase || "https://twitter.com/#!/search?q=%23";
+ options.cashtagClass = options.cashtagClass || DEFAULT_CASHTAG_CLASS;
+ options.cashtagUrlBase = options.cashtagUrlBase || "https://twitter.com/#!/search?q=%24";
+ options.listClass = options.listClass || DEFAULT_LIST_CLASS;
+ options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS;
+ options.usernameUrlBase = options.usernameUrlBase || "https://twitter.com/";
+ options.listUrlBase = options.listUrlBase || "https://twitter.com/";
+ options.htmlAttrs = twttr.txt.extractHtmlAttrsFromOptions(options);
+ options.invisibleTagAttrs = options.invisibleTagAttrs || "style='position:absolute;left:-9999px;'";
+
+ // remap url entities to hash
+ var urlEntities, i, len;
+ if(options.urlEntities) {
+ urlEntities = {};
+ for(i = 0, len = options.urlEntities.length; i < len; i++) {
+ urlEntities[options.urlEntities[i].url] = options.urlEntities[i];
+ }
+ options.urlEntities = urlEntities;
+ }
+
+ var result = "";
+ var beginIndex = 0;
+
+ // sort entities by start index
+ entities.sort(function(a,b){ return a.indices[0] - b.indices[0]; });
+
+ var nonEntity = options.htmlEscapeNonEntities ? twttr.txt.htmlEscape : function(text) {
+ return text;
+ };
+
+ for (var i = 0; i < entities.length; i++) {
+ var entity = entities[i];
+ result += nonEntity(text.substring(beginIndex, entity.indices[0]));
+
+ if (entity.url) {
+ result += twttr.txt.linkToUrl(entity, text, options);
+ } else if (entity.hashtag) {
+ result += twttr.txt.linkToHashtag(entity, text, options);
+ } else if (entity.screenName) {
+ result += twttr.txt.linkToMentionAndList(entity, text, options);
+ } else if (entity.cashtag) {
+ result += twttr.txt.linkToCashtag(entity, text, options);
+ }
+ beginIndex = entity.indices[1];
+ }
+ result += nonEntity(text.substring(beginIndex, text.length));
+ return result;
+ };
+
+ twttr.txt.autoLinkWithJSON = function(text, json, options) {
+ // map JSON entity to twitter-text entity
+ if (json.user_mentions) {
+ for (var i = 0; i < json.user_mentions.length; i++) {
+ // this is a @mention
+ json.user_mentions[i].screenName = json.user_mentions[i].screen_name;
+ }
+ }
+
+ if (json.hashtags) {
+ for (var i = 0; i < json.hashtags.length; i++) {
+ // this is a #hashtag
+ json.hashtags[i].hashtag = json.hashtags[i].text;
+ }
+ }
+
+ if (json.symbols) {
+ for (var i = 0; i < json.symbols.length; i++) {
+ // this is a $CASH tag
+ json.symbols[i].cashtag = json.symbols[i].text;
+ }
+ }
+
+ // concatenate all entities
+ var entities = [];
+ for (var key in json) {
+ entities = entities.concat(json[key]);
+ }
+
+ // modify indices to UTF-16
+ twttr.txt.modifyIndicesFromUnicodeToUTF16(text, entities);
+
+ return twttr.txt.autoLinkEntities(text, entities, options);
+ };
+
+ twttr.txt.extractHtmlAttrsFromOptions = function(options) {
+ var htmlAttrs = {};
+ for (var k in options) {
+ var v = options[k];
+ if (OPTIONS_NOT_ATTRIBUTES[k]) continue;
+ if (BOOLEAN_ATTRIBUTES[k]) {
+ v = v ? k : null;
+ }
+ if (v == null) continue;
+ htmlAttrs[k] = v;
+ }
+ return htmlAttrs;
+ };
+
+ twttr.txt.autoLink = function(text, options) {
+ var entities = twttr.txt.extractEntitiesWithIndices(text, {extractUrlsWithoutProtocol: false});
+ return twttr.txt.autoLinkEntities(text, entities, options);
+ };
+
+ twttr.txt.autoLinkUsernamesOrLists = function(text, options) {
+ var entities = twttr.txt.extractMentionsOrListsWithIndices(text);
+ return twttr.txt.autoLinkEntities(text, entities, options);
+ };
+
+ twttr.txt.autoLinkHashtags = function(text, options) {
+ var entities = twttr.txt.extractHashtagsWithIndices(text);
+ return twttr.txt.autoLinkEntities(text, entities, options);
+ };
+
+ twttr.txt.autoLinkCashtags = function(text, options) {
+ var entities = twttr.txt.extractCashtagsWithIndices(text);
+ return twttr.txt.autoLinkEntities(text, entities, options);
+ };
+
+ twttr.txt.autoLinkUrlsCustom = function(text, options) {
+ var entities = twttr.txt.extractUrlsWithIndices(text, {extractUrlsWithoutProtocol: false});
+ return twttr.txt.autoLinkEntities(text, entities, options);
+ };
+
+ twttr.txt.removeOverlappingEntities = function(entities) {
+ entities.sort(function(a,b){ return a.indices[0] - b.indices[0]; });
+
+ var prev = entities[0];
+ for (var i = 1; i < entities.length; i++) {
+ if (prev.indices[1] > entities[i].indices[0]) {
+ entities.splice(i, 1);
+ i--;
+ } else {
+ prev = entities[i];
+ }
+ }
+ };
+
+ twttr.txt.extractEntitiesWithIndices = function(text, options) {
+ var entities = twttr.txt.extractUrlsWithIndices(text, options)
+ .concat(twttr.txt.extractMentionsOrListsWithIndices(text))
+ .concat(twttr.txt.extractHashtagsWithIndices(text, {checkUrlOverlap: false}))
+ .concat(twttr.txt.extractCashtagsWithIndices(text));
+
+ if (entities.length == 0) {
+ return [];
+ }
+
+ twttr.txt.removeOverlappingEntities(entities);
+ return entities;
+ };
+
+ twttr.txt.extractMentions = function(text) {
+ var screenNamesOnly = [],
+ screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text);
+
+ for (var i = 0; i < screenNamesWithIndices.length; i++) {
+ var screenName = screenNamesWithIndices[i].screenName;
+ screenNamesOnly.push(screenName);
+ }
+
+ return screenNamesOnly;
+ };
+
+ twttr.txt.extractMentionsWithIndices = function(text) {
+ var mentions = [],
+ mentionOrList,
+ mentionsOrLists = twttr.txt.extractMentionsOrListsWithIndices(text);
+
+ for (var i = 0 ; i < mentionsOrLists.length; i++) {
+ mentionOrList = mentionsOrLists[i];
+ if (mentionOrList.listSlug == '') {
+ mentions.push({
+ screenName: mentionOrList.screenName,
+ indices: mentionOrList.indices
+ });
+ }
+ }
+
+ return mentions;
+ };
+
+ /**
+ * Extract list or user mentions.
+ * (Presence of listSlug indicates a list)
+ */
+ twttr.txt.extractMentionsOrListsWithIndices = function(text) {
+ if (!text || !text.match(twttr.txt.regexen.atSigns)) {
+ return [];
+ }
+
+ var possibleNames = [],
+ slashListname;
+
+ text.replace(twttr.txt.regexen.validMentionOrList, function(match, before, atSign, screenName, slashListname, offset, chunk) {
+ var after = chunk.slice(offset + match.length);
+ if (!after.match(twttr.txt.regexen.endMentionMatch)) {
+ slashListname = slashListname || '';
+ var startPosition = offset + before.length;
+ var endPosition = startPosition + screenName.length + slashListname.length + 1;
+ possibleNames.push({
+ screenName: screenName,
+ listSlug: slashListname,
+ indices: [startPosition, endPosition]
+ });
+ }
+ });
+
+ return possibleNames;
+ };
+
+
+ twttr.txt.extractReplies = function(text) {
+ if (!text) {
+ return null;
+ }
+
+ var possibleScreenName = text.match(twttr.txt.regexen.validReply);
+ if (!possibleScreenName ||
+ RegExp.rightContext.match(twttr.txt.regexen.endMentionMatch)) {
+ return null;
+ }
+
+ return possibleScreenName[1];
+ };
+
+ twttr.txt.extractUrls = function(text, options) {
+ var urlsOnly = [],
+ urlsWithIndices = twttr.txt.extractUrlsWithIndices(text, options);
+
+ for (var i = 0; i < urlsWithIndices.length; i++) {
+ urlsOnly.push(urlsWithIndices[i].url);
+ }
+
+ return urlsOnly;
+ };
+
+ twttr.txt.extractUrlsWithIndices = function(text, options) {
+ if (!options) {
+ options = {extractUrlsWithoutProtocol: true};
+ }
+ if (!text || (options.extractUrlsWithoutProtocol ? !text.match(/\./) : !text.match(/:/))) {
+ return [];
+ }
+
+ var urls = [];
+
+ while (twttr.txt.regexen.extractUrl.exec(text)) {
+ var before = RegExp.$2, url = RegExp.$3, protocol = RegExp.$4, domain = RegExp.$5, path = RegExp.$7;
+ var endPosition = twttr.txt.regexen.extractUrl.lastIndex,
+ startPosition = endPosition - url.length;
+
+ // if protocol is missing and domain contains non-ASCII characters,
+ // extract ASCII-only domains.
+ if (!protocol) {
+ if (!options.extractUrlsWithoutProtocol
+ || before.match(twttr.txt.regexen.invalidUrlWithoutProtocolPrecedingChars)) {
+ continue;
+ }
+ var lastUrl = null,
+ asciiEndPosition = 0;
+ domain.replace(twttr.txt.regexen.validAsciiDomain, function(asciiDomain) {
+ var asciiStartPosition = domain.indexOf(asciiDomain, asciiEndPosition);
+ asciiEndPosition = asciiStartPosition + asciiDomain.length;
+ lastUrl = {
+ url: asciiDomain,
+ indices: [startPosition + asciiStartPosition, startPosition + asciiEndPosition]
+ };
+ if (path
+ || asciiDomain.match(twttr.txt.regexen.validSpecialShortDomain)
+ || !asciiDomain.match(twttr.txt.regexen.invalidShortDomain)) {
+ urls.push(lastUrl);
+ }
+ });
+
+ // no ASCII-only domain found. Skip the entire URL.
+ if (lastUrl == null) {
+ continue;
+ }
+
+ // lastUrl only contains domain. Need to add path and query if they exist.
+ if (path) {
+ lastUrl.url = url.replace(domain, lastUrl.url);
+ lastUrl.indices[1] = endPosition;
+ }
+ } else {
+ // In the case of t.co URLs, don't allow additional path characters.
+ if (url.match(twttr.txt.regexen.validTcoUrl)) {
+ url = RegExp.lastMatch;
+ endPosition = startPosition + url.length;
+ }
+ urls.push({
+ url: url,
+ indices: [startPosition, endPosition]
+ });
+ }
+ }
+
+ return urls;
+ };
+
+ twttr.txt.extractHashtags = function(text) {
+ var hashtagsOnly = [],
+ hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text);
+
+ for (var i = 0; i < hashtagsWithIndices.length; i++) {
+ hashtagsOnly.push(hashtagsWithIndices[i].hashtag);
+ }
+
+ return hashtagsOnly;
+ };
+
+ twttr.txt.extractHashtagsWithIndices = function(text, options) {
+ if (!options) {
+ options = {checkUrlOverlap: true};
+ }
+
+ if (!text || !text.match(twttr.txt.regexen.hashSigns)) {
+ return [];
+ }
+
+ var tags = [];
+
+ text.replace(twttr.txt.regexen.validHashtag, function(match, before, hash, hashText, offset, chunk) {
+ var after = chunk.slice(offset + match.length);
+ if (after.match(twttr.txt.regexen.endHashtagMatch))
+ return;
+ var startPosition = offset + before.length;
+ var endPosition = startPosition + hashText.length + 1;
+ tags.push({
+ hashtag: hashText,
+ indices: [startPosition, endPosition]
+ });
+ });
+
+ if (options.checkUrlOverlap) {
+ // also extract URL entities
+ var urls = twttr.txt.extractUrlsWithIndices(text);
+ if (urls.length > 0) {
+ var entities = tags.concat(urls);
+ // remove overlap
+ twttr.txt.removeOverlappingEntities(entities);
+ // only push back hashtags
+ tags = [];
+ for (var i = 0; i < entities.length; i++) {
+ if (entities[i].hashtag) {
+ tags.push(entities[i]);
+ }
+ }
+ }
+ }
+
+ return tags;
+ };
+
+ twttr.txt.extractCashtags = function(text) {
+ var cashtagsOnly = [],
+ cashtagsWithIndices = twttr.txt.extractCashtagsWithIndices(text);
+
+ for (var i = 0; i < cashtagsWithIndices.length; i++) {
+ cashtagsOnly.push(cashtagsWithIndices[i].cashtag);
+ }
+
+ return cashtagsOnly;
+ };
+
+ twttr.txt.extractCashtagsWithIndices = function(text) {
+ if (!text || text.indexOf("$") == -1) {
+ return [];
+ }
+
+ var tags = [];
+
+ text.replace(twttr.txt.regexen.validCashtag, function(match, before, dollar, cashtag, offset, chunk) {
+ var startPosition = offset + before.length;
+ var endPosition = startPosition + cashtag.length + 1;
+ tags.push({
+ cashtag: cashtag,
+ indices: [startPosition, endPosition]
+ });
+ });
+
+ return tags;
+ };
+
+ twttr.txt.modifyIndicesFromUnicodeToUTF16 = function(text, entities) {
+ twttr.txt.convertUnicodeIndices(text, entities, false);
+ };
+
+ twttr.txt.modifyIndicesFromUTF16ToUnicode = function(text, entities) {
+ twttr.txt.convertUnicodeIndices(text, entities, true);
+ };
+
+ twttr.txt.getUnicodeTextLength = function(text) {
+ return text.replace(twttr.txt.regexen.non_bmp_code_pairs, ' ').length;
+ };
+
+ twttr.txt.convertUnicodeIndices = function(text, entities, indicesInUTF16) {
+ if (entities.length == 0) {
+ return;
+ }
+
+ var charIndex = 0;
+ var codePointIndex = 0;
+
+ // sort entities by start index
+ entities.sort(function(a,b){ return a.indices[0] - b.indices[0]; });
+ var entityIndex = 0;
+ var entity = entities[0];
+
+ while (charIndex < text.length) {
+ if (entity.indices[0] == (indicesInUTF16 ? charIndex : codePointIndex)) {
+ var len = entity.indices[1] - entity.indices[0];
+ entity.indices[0] = indicesInUTF16 ? codePointIndex : charIndex;
+ entity.indices[1] = entity.indices[0] + len;
+
+ entityIndex++;
+ if (entityIndex == entities.length) {
+ // no more entity
+ break;
+ }
+ entity = entities[entityIndex];
+ }
+
+ var c = text.charCodeAt(charIndex);
+ if (0xD800 <= c && c <= 0xDBFF && charIndex < text.length - 1) {
+ // Found high surrogate char
+ c = text.charCodeAt(charIndex + 1);
+ if (0xDC00 <= c && c <= 0xDFFF) {
+ // Found surrogate pair
+ charIndex++;
+ }
+ }
+ codePointIndex++;
+ charIndex++;
+ }
+ };
+
+ // this essentially does text.split(/<|>/)
+ // except that won't work in IE, where empty strings are ommitted
+ // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others
+ // but "<<".split("<") => ["", "", ""]
+ twttr.txt.splitTags = function(text) {
+ var firstSplits = text.split("<"),
+ secondSplits,
+ allSplits = [],
+ split;
+
+ for (var i = 0; i < firstSplits.length; i += 1) {
+ split = firstSplits[i];
+ if (!split) {
+ allSplits.push("");
+ } else {
+ secondSplits = split.split(">");
+ for (var j = 0; j < secondSplits.length; j += 1) {
+ allSplits.push(secondSplits[j]);
+ }
+ }
+ }
+
+ return allSplits;
+ };
+
+ twttr.txt.hitHighlight = function(text, hits, options) {
+ var defaultHighlightTag = "em";
+
+ hits = hits || [];
+ options = options || {};
+
+ if (hits.length === 0) {
+ return text;
+ }
+
+ var tagName = options.tag || defaultHighlightTag,
+ tags = ["<" + tagName + ">", "" + tagName + ">"],
+ chunks = twttr.txt.splitTags(text),
+ i,
+ j,
+ result = "",
+ chunkIndex = 0,
+ chunk = chunks[0],
+ prevChunksLen = 0,
+ chunkCursor = 0,
+ startInChunk = false,
+ chunkChars = chunk,
+ flatHits = [],
+ index,
+ hit,
+ tag,
+ placed,
+ hitSpot;
+
+ for (i = 0; i < hits.length; i += 1) {
+ for (j = 0; j < hits[i].length; j += 1) {
+ flatHits.push(hits[i][j]);
+ }
+ }
+
+ for (index = 0; index < flatHits.length; index += 1) {
+ hit = flatHits[index];
+ tag = tags[index % 2];
+ placed = false;
+
+ while (chunk != null && hit >= prevChunksLen + chunk.length) {
+ result += chunkChars.slice(chunkCursor);
+ if (startInChunk && hit === prevChunksLen + chunkChars.length) {
+ result += tag;
+ placed = true;
+ }
+
+ if (chunks[chunkIndex + 1]) {
+ result += "<" + chunks[chunkIndex + 1] + ">";
+ }
+
+ prevChunksLen += chunkChars.length;
+ chunkCursor = 0;
+ chunkIndex += 2;
+ chunk = chunks[chunkIndex];
+ chunkChars = chunk;
+ startInChunk = false;
+ }
+
+ if (!placed && chunk != null) {
+ hitSpot = hit - prevChunksLen;
+ result += chunkChars.slice(chunkCursor, hitSpot) + tag;
+ chunkCursor = hitSpot;
+ if (index % 2 === 0) {
+ startInChunk = true;
+ } else {
+ startInChunk = false;
+ }
+ } else if(!placed) {
+ placed = true;
+ result += tag;
+ }
+ }
+
+ if (chunk != null) {
+ if (chunkCursor < chunkChars.length) {
+ result += chunkChars.slice(chunkCursor);
+ }
+ for (index = chunkIndex + 1; index < chunks.length; index += 1) {
+ result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">");
+ }
+ }
+
+ return result;
+ };
+
+ var MAX_LENGTH = 140;
+
+ // Returns the length of Tweet text with consideration to t.co URL replacement
+ // and chars outside the basic multilingual plane that use 2 UTF16 code points
+ twttr.txt.getTweetLength = function(text, options) {
+ if (!options) {
+ options = {
+ // These come from https://api.twitter.com/1.1/help/configuration.json
+ // described by https://dev.twitter.com/rest/reference/get/help/configuration
+ short_url_length: 23,
+ short_url_length_https: 23
+ };
+ }
+ var textLength = twttr.txt.getUnicodeTextLength(text),
+ urlsWithIndices = twttr.txt.extractUrlsWithIndices(text);
+ twttr.txt.modifyIndicesFromUTF16ToUnicode(text, urlsWithIndices);
+
+ for (var i = 0; i < urlsWithIndices.length; i++) {
+ // Subtract the length of the original URL
+ textLength += urlsWithIndices[i].indices[0] - urlsWithIndices[i].indices[1];
+
+ // Add 23 characters for URL starting with https://
+ // http:// URLs still use https://t.co so they are 23 characters as well
+ if (urlsWithIndices[i].url.toLowerCase().match(twttr.txt.regexen.urlHasHttps)) {
+ textLength += options.short_url_length_https;
+ } else {
+ textLength += options.short_url_length;
+ }
+ }
+
+ return textLength;
+ };
+
+ // Check the text for any reason that it may not be valid as a Tweet. This is meant as a pre-validation
+ // before posting to api.twitter.com. There are several server-side reasons for Tweets to fail but this pre-validation
+ // will allow quicker feedback.
+ //
+ // Returns false if this text is valid. Otherwise one of the following strings will be returned:
+ //
+ // "too_long": if the text is too long
+ // "empty": if the text is nil or empty
+ // "invalid_characters": if the text contains non-Unicode or any of the disallowed Unicode characters
+ twttr.txt.isInvalidTweet = function(text) {
+ if (!text) {
+ return "empty";
+ }
+
+ // Determine max length independent of URL length
+ if (twttr.txt.getTweetLength(text) > MAX_LENGTH) {
+ return "too_long";
+ }
+
+ if (twttr.txt.hasInvalidCharacters(text)) {
+ return "invalid_characters";
+ }
+
+ return false;
+ };
+
+ twttr.txt.hasInvalidCharacters = function(text) {
+ return twttr.txt.regexen.invalid_chars.test(text);
+ };
+
+ twttr.txt.isValidTweetText = function(text) {
+ return !twttr.txt.isInvalidTweet(text);
+ };
+
+ twttr.txt.isValidUsername = function(username) {
+ if (!username) {
+ return false;
+ }
+
+ var extracted = twttr.txt.extractMentions(username);
+
+ // Should extract the username minus the @ sign, hence the .slice(1)
+ return extracted.length === 1 && extracted[0] === username.slice(1);
+ };
+
+ var VALID_LIST_RE = regexSupplant(/^#{validMentionOrList}$/);
+
+ twttr.txt.isValidList = function(usernameList) {
+ var match = usernameList.match(VALID_LIST_RE);
+
+ // Must have matched and had nothing before or after
+ return !!(match && match[1] == "" && match[4]);
+ };
+
+ twttr.txt.isValidHashtag = function(hashtag) {
+ if (!hashtag) {
+ return false;
+ }
+
+ var extracted = twttr.txt.extractHashtags(hashtag);
+
+ // Should extract the hashtag minus the # sign, hence the .slice(1)
+ return extracted.length === 1 && extracted[0] === hashtag.slice(1);
+ };
+
+ twttr.txt.isValidUrl = function(url, unicodeDomains, requireProtocol) {
+ if (unicodeDomains == null) {
+ unicodeDomains = true;
+ }
+
+ if (requireProtocol == null) {
+ requireProtocol = true;
+ }
+
+ if (!url) {
+ return false;
+ }
+
+ var urlParts = url.match(twttr.txt.regexen.validateUrlUnencoded);
+
+ if (!urlParts || urlParts[0] !== url) {
+ return false;
+ }
+
+ var scheme = urlParts[1],
+ authority = urlParts[2],
+ path = urlParts[3],
+ query = urlParts[4],
+ fragment = urlParts[5];
+
+ if (!(
+ (!requireProtocol || (isValidMatch(scheme, twttr.txt.regexen.validateUrlScheme) && scheme.match(/^https?$/i))) &&
+ isValidMatch(path, twttr.txt.regexen.validateUrlPath) &&
+ isValidMatch(query, twttr.txt.regexen.validateUrlQuery, true) &&
+ isValidMatch(fragment, twttr.txt.regexen.validateUrlFragment, true)
+ )) {
+ return false;
+ }
+
+ return (unicodeDomains && isValidMatch(authority, twttr.txt.regexen.validateUrlUnicodeAuthority)) ||
+ (!unicodeDomains && isValidMatch(authority, twttr.txt.regexen.validateUrlAuthority));
+ };
+
+ function isValidMatch(string, regex, optional) {
+ if (!optional) {
+ // RegExp["$&"] is the text of the last match
+ // blank strings are ok, but are falsy, so we check stringiness instead of truthiness
+ return ((typeof string === "string") && string.match(regex) && RegExp["$&"] === string);
+ }
+
+ // RegExp["$&"] is the text of the last match
+ return (!string || (string.match(regex) && RegExp["$&"] === string));
+ }
+
+ if (typeof module != 'undefined' && module.exports) {
+ module.exports = twttr.txt;
+ }
+
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (twttr.txt),
+ __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
+ (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
+ __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ }
+
+ if (typeof window != 'undefined') {
+ if (window.twttr) {
+ for (var prop in twttr) {
+ window.twttr[prop] = twttr[prop];
+ }
+ } else {
+ window.twttr = twttr;
+ }
+ }
+})();
+
+
/***/ })
/******/ ]);
\ No newline at end of file
diff --git a/public/twitter.ico b/public/twitter.ico
new file mode 100644
index 0000000..b6dbef4
Binary files /dev/null and b/public/twitter.ico differ
diff --git a/resources/assets/js/bootstrap.js b/resources/assets/js/bootstrap.js
index df06603..0b3cd2a 100644
--- a/resources/assets/js/bootstrap.js
+++ b/resources/assets/js/bootstrap.js
@@ -56,3 +56,5 @@ window.Echo = new Echo({
cluster: 'us2',
encrypted: true
});
+
+window.twitter = require('twitter-text')
diff --git a/resources/assets/js/components/Scorecard.vue b/resources/assets/js/components/Scorecard.vue
index defe2b8..426e571 100644
--- a/resources/assets/js/components/Scorecard.vue
+++ b/resources/assets/js/components/Scorecard.vue
@@ -31,10 +31,16 @@
-
@@ -169,6 +172,9 @@ module.exports = {
centers: [],
lines: [],
replyText: '',
+ replyTextCharsRemaining: 140,
+ replyInProgress: false,
+ showReplyBox: true,
selectedDocument: null,
selectedTransitCenter: null,
selectedTransitLine: null,
@@ -203,7 +209,7 @@ module.exports = {
}
},
isTweetReplyDisabled() {
- return this.replyText == '';
+ return this.replyText == '' || this.replyInProgress || this.replyTextCharsRemaining < 0;
}
},
methods: {
@@ -216,6 +222,7 @@ module.exports = {
this.selectedM5Singing = false;
this.selectedM5Tipping = false;
this.replyText = '';
+ this.showReplyBox = true;
},
dismiss() {
this.clearState();
@@ -268,6 +275,21 @@ module.exports = {
this.clearState();
this.$emit('complete');
}.bind(this));
+ },
+ sendReply() {
+ this.replyInProgress = true;
+
+ $.post("/dashboard/reply", {
+ tweet_id: this.tweet.tweet_id,
+ text: this.replyText
+ }, function(response){
+ if(response.result == "ok") {
+ this.replyText = '';
+ }
+ this.replyInProgress = false;
+ }.bind(this));
+
+ // setTimeout(function(){ this.replyInProgress = false; this.showReplyBox = false; }.bind(this), 1000);
}
},
watch: {
@@ -279,6 +301,9 @@ module.exports = {
$(".multi-photo .photo").featherlight();
}.bind(this));
}
+ },
+ replyText: function(val) {
+ this.replyTextCharsRemaining = 140 - twitter.getTweetLength(val);
}
},
created() {
diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss
index 8d26907..9611d12 100644
--- a/resources/assets/sass/app.scss
+++ b/resources/assets/sass/app.scss
@@ -147,12 +147,21 @@
padding: 8px;
background-color: $body-bg;
- button {
- float: right;
- }
+ .bottom {
+ display: flex;
+ align-items: center;
+
+ .fill {
+ flex: 1;
+ }
+
+ img {
+ margin-right: 4px;
+ }
- .clear {
- clear: both;
+ .remaining-chars {
+ margin-right: 10px;
+ }
}
}
diff --git a/routes/web.php b/routes/web.php
index f21aec5..9fffdbe 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -24,6 +24,7 @@ Route::get('/dashboard/dropdowns', 'DashboardController@load_dropdowns');
Route::post('/dashboard/claim-tweet', 'DashboardController@claim_tweet');
Route::post('/dashboard/reject-tweet', 'DashboardController@reject_tweet');
Route::post('/dashboard/score-tweet', 'DashboardController@score_tweet');
+Route::post('/dashboard/reply', 'DashboardController@reply_to_tweet');
Route::get('/slideshow', 'SlideshowController@slideshow')->name('slideshow');