├── .gitignore ├── res ├── icon.xcf └── promo.xcf ├── img ├── icon16.png ├── icon48.png └── icon128.png ├── settings.example.js ├── onload.js ├── background.html ├── Makefile ├── README ├── chrome_ex_oauth.html ├── PRIVACY ├── manifest.json ├── LICENSE ├── background.js ├── LICENSE.chrome_ex_oauth ├── LICENSE.twitter-text ├── chrome_ex_oauthsimple.js ├── chrome_ex_oauth.js └── twitter-text.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | settings.js 3 | -------------------------------------------------------------------------------- /res/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyer/chromnitweet/HEAD/res/icon.xcf -------------------------------------------------------------------------------- /img/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyer/chromnitweet/HEAD/img/icon16.png -------------------------------------------------------------------------------- /img/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyer/chromnitweet/HEAD/img/icon48.png -------------------------------------------------------------------------------- /res/promo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyer/chromnitweet/HEAD/res/promo.xcf -------------------------------------------------------------------------------- /img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyer/chromnitweet/HEAD/img/icon128.png -------------------------------------------------------------------------------- /settings.example.js: -------------------------------------------------------------------------------- 1 | const TWITTER_CONSUMER_KEY = 'YOUR KEY HERE'; 2 | const TWITTER_CONSUMER_SECRET = 'YOUR SECRET HERE'; 3 | -------------------------------------------------------------------------------- /onload.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | window.onload = function() { 6 | ChromeExOAuth.initCallbackPage(); 7 | } 8 | -------------------------------------------------------------------------------- /background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ZIP=zip 2 | 3 | # compiled files 4 | PACKAGE=chromnitweet.zip 5 | 6 | # source files 7 | MANIFEST=manifest.json 8 | HTML=background.html chrome_ex_oauth.html 9 | PNG=img/* 10 | JS=background.js settings.js chrome_ex_oauth.js chrome_ex_oauthsimple.js onload.js twitter-text.js 11 | 12 | all: $(PACKAGE) 13 | 14 | $(PACKAGE): $(MANIFEST) $(HTML) $(JS) $(PNG) 15 | $(ZIP) $@ $^ 16 | 17 | clean: 18 | $(RM) $(PACKAGE) 19 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Chromnitweet is a Google Chrome (or Chromium) extension that acts as a Twitter 2 | client, allowing the user to post tweet (status update or direct message) 3 | directly from the Omnibox (Omnibar, awesome bar, URL bar, location bar, etc.) 4 | with the syntax "tw [some string of 280 or less characters]". 5 | 6 | https://chrome.google.com/webstore/detail/ofhffnelacegjkgcbohojhebkbehikep?hl=en-US 7 | 8 | Useless fact: Chromnitweet is a portmanteau of Chrome, Omnibox and Tweet. 9 | 10 | INSTALLATION 11 | ============ 12 | 13 | - Copy settings.example.js to settings.js. 14 | - Open settings.js and add your Twitter OAuth info. 15 | - If you have GNU make and zip installed, run `make` to create a package. 16 | -------------------------------------------------------------------------------- /chrome_ex_oauth.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | OAuth Redirect Page 10 | 16 | 17 | 18 | 19 | 20 | 21 | Redirecting... 22 | 23 | 24 | -------------------------------------------------------------------------------- /PRIVACY: -------------------------------------------------------------------------------- 1 | CHROMNITWEET PRIVACY POLICY 2 | 3 | Chromnitweet is a Twitter client installed on your computer as a Chrome 4 | extension. All communication performed in Chromnitweet is directly between 5 | Twitter and its users. Chromnitweet does not rely on, or have access to, any 6 | third-party servers. Therefore, Chromnitweet and its creators do not, and can 7 | not, record, keep or access any data on any of its users. 8 | 9 | CHANGES TO THIS PRIVACY POLICY 10 | 11 | We may update our Privacy Policy from time to time. Thus, we advise you to 12 | review this page periodically for any changes. We will notify you of any changes 13 | by posting the new Privacy Policy on this page. These changes are effective 14 | immediately, after they are posted on this page. 15 | 16 | CONTACT US 17 | 18 | Questions and suggestions about this privacy policy are welcome by email at 19 | caseydwyer@gmail.com. 20 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chromnitweet", 3 | "short_name": "Chromnitweet", 4 | "manifest_version": 2, 5 | "version": "2.8.3", 6 | "description": "Update your Twitter status right from Chrome's Omnibox (URL bar).", 7 | "omnibox": {"keyword": "tw"}, 8 | "icons": { 9 | "128": "img/icon128.png", 10 | "48": "img/icon48.png", 11 | "16": "img/icon16.png" 12 | }, 13 | "background": { 14 | "scripts": [ 15 | "chrome_ex_oauthsimple.js", 16 | "chrome_ex_oauth.js", 17 | "onload.js", 18 | "twitter-text.js", 19 | "settings.js", 20 | "background.js" 21 | ], 22 | "persistent": false 23 | }, 24 | "web_accessible_resources": ["chrome_ex_oauth.html"], 25 | "permissions": ["notifications", "tabs", "https://api.twitter.com/", "https://*.twimg.com/"], 26 | "content_security_policy": "script-src 'self'; object-src 'self'; img-src 'self' https://*.twimg.com" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014, The FreeKCD Project. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | This software is provided by the copyright holders and contributors "as 14 | is" and any express or implied warranties, including, but not limited 15 | to, the implied warranties of merchantability and fitness for a 16 | particular purpose are disclaimed. In no event shall the copyright 17 | holder or contributors be liable for any direct, indirect, incidental, 18 | special, exemplary, or consequential damages (including, but not limited 19 | to, procurement of substitute goods or services; loss of use, data, or 20 | profits; or business interruption) however caused and on any theory of 21 | liability, whether in contract, strict liability, or tort (including 22 | negligence or otherwise) arising in any way out of the use of this 23 | software, even if advised of the possibility of such damage. 24 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) The FreeKCD Project. See LICENSE. 3 | */ 4 | 5 | const TWITTER_UPDATE_URL = 'https://api.twitter.com/1.1/statuses/update.json'; 6 | const NOTIFICATION_TIMEOUT = 3000; 7 | 8 | var oauth = ChromeExOAuth.initBackgroundPage({ 9 | request_url: 'https://api.twitter.com/oauth/request_token', 10 | authorize_url: 'https://api.twitter.com/oauth/authorize', 11 | access_url: 'https://api.twitter.com/oauth/access_token', 12 | consumer_key: TWITTER_CONSUMER_KEY, 13 | consumer_secret: TWITTER_CONSUMER_SECRET, 14 | scope: TWITTER_UPDATE_URL, 15 | app_name: 'Chromnitweet' 16 | }); 17 | 18 | function notify(iconUrl, title, message) { 19 | chrome.notifications.create('chromnitweet', { 20 | type: 'basic', 21 | iconUrl: iconUrl, 22 | title: title, 23 | message: message 24 | }, function(id) { 25 | window.setTimeout(function() { 26 | chrome.notifications.clear(id, function() {}); 27 | }, NOTIFICATION_TIMEOUT); 28 | }); 29 | } 30 | 31 | oauth.authorize(function() { 32 | chrome.omnibox.onInputChanged.addListener(function(text) { 33 | chrome.omnibox.setDefaultSuggestion({ 34 | description: String(280 - twttr.txt.getTweetLength(text)) + ' characters remaining.' 35 | }); 36 | }); 37 | chrome.omnibox.onInputEntered.addListener(function(text, disposition) { 38 | function sendSignedRequestCallback(response, xhr) { 39 | var result = JSON.parse(response); 40 | if (result.errors !== undefined) { 41 | notify('img/icon128.png', 'Oops! There was an error.', 42 | result.errors[0].message); 43 | } else { 44 | notify(result.user.profile_image_url_https, result.user.name, 45 | result.text); 46 | } 47 | } 48 | oauth.sendSignedRequest(TWITTER_UPDATE_URL, sendSignedRequestCallback, { 49 | method: 'POST', 50 | parameters: {status: text} 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /LICENSE.chrome_ex_oauth: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Google Inc. nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | This extension uses code from the following two JavaScript libraries: 30 | 31 | http://unitedheroes.net/OAuthSimple/js/OAuthSimple.js 32 | ===================================================== 33 | /* OAuthSimple 34 | * A simpler version of OAuth 35 | * 36 | * author: jr conlin 37 | * mail: src@anticipatr.com 38 | * copyright: unitedHeroes.net 39 | * version: 1.0 40 | * url: http://unitedHeroes.net/OAuthSimple 41 | * 42 | * Copyright (c) 2009, unitedHeroes.net 43 | * All rights reserved. 44 | * 45 | * Redistribution and use in source and binary forms, with or without 46 | * modification, are permitted provided that the following conditions are met: 47 | * * Redistributions of source code must retain the above copyright 48 | * notice, this list of conditions and the following disclaimer. 49 | * * Redistributions in binary form must reproduce the above copyright 50 | * notice, this list of conditions and the following disclaimer in the 51 | * documentation and/or other materials provided with the distribution. 52 | * * Neither the name of the unitedHeroes.net nor the 53 | * names of its contributors may be used to endorse or promote products 54 | * derived from this software without specific prior written permission. 55 | * 56 | * THIS SOFTWARE IS PROVIDED BY UNITEDHEROES.NET ''AS IS'' AND ANY 57 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 58 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 59 | * DISCLAIMED. IN NO EVENT SHALL UNITEDHEROES.NET BE LIABLE FOR ANY 60 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 61 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 62 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 63 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 64 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 65 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 66 | */ 67 | 68 | http://pajhome.org.uk/crypt/md5/sha1.js 69 | ======================================= 70 | /* 71 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 72 | * in FIPS PUB 180-1 73 | * Version 2.1a Copyright Paul Johnston 2000 - 2002. 74 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 75 | * Distributed under the BSD License 76 | * See http://pajhome.org.uk/crypt/md5 for details. 77 | */ 78 | -------------------------------------------------------------------------------- /LICENSE.twitter-text: -------------------------------------------------------------------------------- 1 | Copyright 2011 Twitter, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this work except in compliance with the License. 5 | You may obtain a copy of the License below, or at: 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Apache License 16 | Version 2.0, January 2004 17 | http://www.apache.org/licenses/ 18 | 19 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 20 | 21 | 1. Definitions. 22 | 23 | "License" shall mean the terms and conditions for use, reproduction, 24 | and distribution as defined by Sections 1 through 9 of this document. 25 | 26 | "Licensor" shall mean the copyright owner or entity authorized by 27 | the copyright owner that is granting the License. 28 | 29 | "Legal Entity" shall mean the union of the acting entity and all 30 | other entities that control, are controlled by, or are under common 31 | control with that entity. For the purposes of this definition, 32 | "control" means (i) the power, direct or indirect, to cause the 33 | direction or management of such entity, whether by contract or 34 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 35 | outstanding shares, or (iii) beneficial ownership of such entity. 36 | 37 | "You" (or "Your") shall mean an individual or Legal Entity 38 | exercising permissions granted by this License. 39 | 40 | "Source" form shall mean the preferred form for making modifications, 41 | including but not limited to software source code, documentation 42 | source, and configuration files. 43 | 44 | "Object" form shall mean any form resulting from mechanical 45 | transformation or translation of a Source form, including but 46 | not limited to compiled object code, generated documentation, 47 | and conversions to other media types. 48 | 49 | "Work" shall mean the work of authorship, whether in Source or 50 | Object form, made available under the License, as indicated by a 51 | copyright notice that is included in or attached to the work 52 | (an example is provided in the Appendix below). 53 | 54 | "Derivative Works" shall mean any work, whether in Source or Object 55 | form, that is based on (or derived from) the Work and for which the 56 | editorial revisions, annotations, elaborations, or other modifications 57 | represent, as a whole, an original work of authorship. For the purposes 58 | of this License, Derivative Works shall not include works that remain 59 | separable from, or merely link (or bind by name) to the interfaces of, 60 | the Work and Derivative Works thereof. 61 | 62 | "Contribution" shall mean any work of authorship, including 63 | the original version of the Work and any modifications or additions 64 | to that Work or Derivative Works thereof, that is intentionally 65 | submitted to Licensor for inclusion in the Work by the copyright owner 66 | or by an individual or Legal Entity authorized to submit on behalf of 67 | the copyright owner. For the purposes of this definition, "submitted" 68 | means any form of electronic, verbal, or written communication sent 69 | to the Licensor or its representatives, including but not limited to 70 | communication on electronic mailing lists, source code control systems, 71 | and issue tracking systems that are managed by, or on behalf of, the 72 | Licensor for the purpose of discussing and improving the Work, but 73 | excluding communication that is conspicuously marked or otherwise 74 | designated in writing by the copyright owner as "Not a Contribution." 75 | 76 | "Contributor" shall mean Licensor and any individual or Legal Entity 77 | on behalf of whom a Contribution has been received by Licensor and 78 | subsequently incorporated within the Work. 79 | 80 | 2. Grant of Copyright License. Subject to the terms and conditions of 81 | this License, each Contributor hereby grants to You a perpetual, 82 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 83 | copyright license to reproduce, prepare Derivative Works of, 84 | publicly display, publicly perform, sublicense, and distribute the 85 | Work and such Derivative Works in Source or Object form. 86 | 87 | 3. Grant of Patent License. Subject to the terms and conditions of 88 | this License, each Contributor hereby grants to You a perpetual, 89 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 90 | (except as stated in this section) patent license to make, have made, 91 | use, offer to sell, sell, import, and otherwise transfer the Work, 92 | where such license applies only to those patent claims licensable 93 | by such Contributor that are necessarily infringed by their 94 | Contribution(s) alone or by combination of their Contribution(s) 95 | with the Work to which such Contribution(s) was submitted. If You 96 | institute patent litigation against any entity (including a 97 | cross-claim or counterclaim in a lawsuit) alleging that the Work 98 | or a Contribution incorporated within the Work constitutes direct 99 | or contributory patent infringement, then any patent licenses 100 | granted to You under this License for that Work shall terminate 101 | as of the date such litigation is filed. 102 | 103 | 4. Redistribution. You may reproduce and distribute copies of the 104 | Work or Derivative Works thereof in any medium, with or without 105 | modifications, and in Source or Object form, provided that You 106 | meet the following conditions: 107 | 108 | (a) You must give any other recipients of the Work or 109 | Derivative Works a copy of this License; and 110 | 111 | (b) You must cause any modified files to carry prominent notices 112 | stating that You changed the files; and 113 | 114 | (c) You must retain, in the Source form of any Derivative Works 115 | that You distribute, all copyright, patent, trademark, and 116 | attribution notices from the Source form of the Work, 117 | excluding those notices that do not pertain to any part of 118 | the Derivative Works; and 119 | 120 | (d) If the Work includes a "NOTICE" text file as part of its 121 | distribution, then any Derivative Works that You distribute must 122 | include a readable copy of the attribution notices contained 123 | within such NOTICE file, excluding those notices that do not 124 | pertain to any part of the Derivative Works, in at least one 125 | of the following places: within a NOTICE text file distributed 126 | as part of the Derivative Works; within the Source form or 127 | documentation, if provided along with the Derivative Works; or, 128 | within a display generated by the Derivative Works, if and 129 | wherever such third-party notices normally appear. The contents 130 | of the NOTICE file are for informational purposes only and 131 | do not modify the License. You may add Your own attribution 132 | notices within Derivative Works that You distribute, alongside 133 | or as an addendum to the NOTICE text from the Work, provided 134 | that such additional attribution notices cannot be construed 135 | as modifying the License. 136 | 137 | You may add Your own copyright statement to Your modifications and 138 | may provide additional or different license terms and conditions 139 | for use, reproduction, or distribution of Your modifications, or 140 | for any such Derivative Works as a whole, provided Your use, 141 | reproduction, and distribution of the Work otherwise complies with 142 | the conditions stated in this License. 143 | 144 | 5. Submission of Contributions. Unless You explicitly state otherwise, 145 | any Contribution intentionally submitted for inclusion in the Work 146 | by You to the Licensor shall be under the terms and conditions of 147 | this License, without any additional terms or conditions. 148 | Notwithstanding the above, nothing herein shall supersede or modify 149 | the terms of any separate license agreement you may have executed 150 | with Licensor regarding such Contributions. 151 | 152 | 6. Trademarks. This License does not grant permission to use the trade 153 | names, trademarks, service marks, or product names of the Licensor, 154 | except as required for reasonable and customary use in describing the 155 | origin of the Work and reproducing the content of the NOTICE file. 156 | 157 | 7. Disclaimer of Warranty. Unless required by applicable law or 158 | agreed to in writing, Licensor provides the Work (and each 159 | Contributor provides its Contributions) on an "AS IS" BASIS, 160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 161 | implied, including, without limitation, any warranties or conditions 162 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 163 | PARTICULAR PURPOSE. You are solely responsible for determining the 164 | appropriateness of using or redistributing the Work and assume any 165 | risks associated with Your exercise of permissions under this License. 166 | 167 | 8. Limitation of Liability. In no event and under no legal theory, 168 | whether in tort (including negligence), contract, or otherwise, 169 | unless required by applicable law (such as deliberate and grossly 170 | negligent acts) or agreed to in writing, shall any Contributor be 171 | liable to You for damages, including any direct, indirect, special, 172 | incidental, or consequential damages of any character arising as a 173 | result of this License or out of the use or inability to use the 174 | Work (including but not limited to damages for loss of goodwill, 175 | work stoppage, computer failure or malfunction, or any and all 176 | other commercial damages or losses), even if such Contributor 177 | has been advised of the possibility of such damages. 178 | 179 | 9. Accepting Warranty or Additional Liability. While redistributing 180 | the Work or Derivative Works thereof, You may choose to offer, 181 | and charge a fee for, acceptance of support, warranty, indemnity, 182 | or other liability obligations and/or rights consistent with this 183 | License. However, in accepting such obligations, You may act only 184 | on Your own behalf and on Your sole responsibility, not on behalf 185 | of any other Contributor, and only if You agree to indemnify, 186 | defend, and hold each Contributor harmless for any liability 187 | incurred by, or claims asserted against, such Contributor by reason 188 | of your accepting any such warranty or additional liability. 189 | -------------------------------------------------------------------------------- /chrome_ex_oauthsimple.js: -------------------------------------------------------------------------------- 1 | /* OAuthSimple 2 | * A simpler version of OAuth 3 | * 4 | * author: jr conlin 5 | * mail: src@anticipatr.com 6 | * copyright: unitedHeroes.net 7 | * version: 1.0 8 | * url: http://unitedHeroes.net/OAuthSimple 9 | * 10 | * Copyright (c) 2009, unitedHeroes.net 11 | * All rights reserved. 12 | * 13 | * Redistribution and use in source and binary forms, with or without 14 | * modification, are permitted provided that the following conditions are met: 15 | * * Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * * Redistributions in binary form must reproduce the above copyright 18 | * notice, this list of conditions and the following disclaimer in the 19 | * documentation and/or other materials provided with the distribution. 20 | * * Neither the name of the unitedHeroes.net nor the 21 | * names of its contributors may be used to endorse or promote products 22 | * derived from this software without specific prior written permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED BY UNITEDHEROES.NET ''AS IS'' AND ANY 25 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | * DISCLAIMED. IN NO EVENT SHALL UNITEDHEROES.NET BE LIABLE FOR ANY 28 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 31 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | var OAuthSimple; 36 | 37 | if (OAuthSimple === undefined) 38 | { 39 | /* Simple OAuth 40 | * 41 | * This class only builds the OAuth elements, it does not do the actual 42 | * transmission or reception of the tokens. It does not validate elements 43 | * of the token. It is for client use only. 44 | * 45 | * api_key is the API key, also known as the OAuth consumer key 46 | * shared_secret is the shared secret (duh). 47 | * 48 | * Both the api_key and shared_secret are generally provided by the site 49 | * offering OAuth services. You need to specify them at object creation 50 | * because nobody ing uses OAuth without that minimal set of 51 | * signatures. 52 | * 53 | * If you want to use the higher order security that comes from the 54 | * OAuth token (sorry, I don't provide the functions to fetch that because 55 | * sites aren't horribly consistent about how they offer that), you need to 56 | * pass those in either with .setTokensAndSecrets() or as an argument to the 57 | * .sign() or .getHeaderString() functions. 58 | * 59 | * Example: 60 | 61 | var oauthObject = OAuthSimple().sign({path:'http://example.com/rest/', 62 | parameters: 'foo=bar&gorp=banana', 63 | signatures:{ 64 | api_key:'12345abcd', 65 | shared_secret:'xyz-5309' 66 | }}); 67 | document.getElementById('someLink').href=oauthObject.signed_url; 68 | 69 | * 70 | * that will sign as a "GET" using "SHA1-MAC" the url. If you need more than 71 | * that, read on, McDuff. 72 | */ 73 | 74 | /** OAuthSimple creator 75 | * 76 | * Create an instance of OAuthSimple 77 | * 78 | * @param api_key {string} The API Key (sometimes referred to as the consumer key) This value is usually supplied by the site you wish to use. 79 | * @param shared_secret (string) The shared secret. This value is also usually provided by the site you wish to use. 80 | */ 81 | OAuthSimple = function (consumer_key,shared_secret) 82 | { 83 | /* if (api_key == undefined) 84 | throw("Missing argument: api_key (oauth_consumer_key) for OAuthSimple. This is usually provided by the hosting site."); 85 | if (shared_secret == undefined) 86 | throw("Missing argument: shared_secret (shared secret) for OAuthSimple. This is usually provided by the hosting site."); 87 | */ this._secrets={}; 88 | this._parameters={}; 89 | 90 | // General configuration options. 91 | if (consumer_key !== undefined) { 92 | this._secrets['consumer_key'] = consumer_key; 93 | } 94 | if (shared_secret !== undefined) { 95 | this._secrets['shared_secret'] = shared_secret; 96 | } 97 | this._default_signature_method= "HMAC-SHA1"; 98 | this._action = "GET"; 99 | this._nonce_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 100 | 101 | 102 | this.reset = function() { 103 | this._parameters={}; 104 | this._path=undefined; 105 | return this; 106 | }; 107 | 108 | /** set the parameters either from a hash or a string 109 | * 110 | * @param {string,object} List of parameters for the call, this can either be a URI string (e.g. "foo=bar&gorp=banana" or an object/hash) 111 | */ 112 | this.setParameters = function (parameters) { 113 | if (parameters === undefined) { 114 | parameters = {}; 115 | } 116 | if (typeof(parameters) == 'string') { 117 | parameters=this._parseParameterString(parameters); 118 | } 119 | this._parameters = parameters; 120 | if (this._parameters['oauth_nonce'] === undefined) { 121 | this._getNonce(); 122 | } 123 | if (this._parameters['oauth_timestamp'] === undefined) { 124 | this._getTimestamp(); 125 | } 126 | if (this._parameters['oauth_method'] === undefined) { 127 | this.setSignatureMethod(); 128 | } 129 | if (this._parameters['oauth_consumer_key'] === undefined) { 130 | this._getApiKey(); 131 | } 132 | if(this._parameters['oauth_token'] === undefined) { 133 | this._getAccessToken(); 134 | } 135 | 136 | return this; 137 | }; 138 | 139 | /** convienence method for setParameters 140 | * 141 | * @param parameters {string,object} See .setParameters 142 | */ 143 | this.setQueryString = function (parameters) { 144 | return this.setParameters(parameters); 145 | }; 146 | 147 | /** Set the target URL (does not include the parameters) 148 | * 149 | * @param path {string} the fully qualified URI (excluding query arguments) (e.g "http://example.org/foo") 150 | */ 151 | this.setURL = function (path) { 152 | if (path == '') { 153 | throw ('No path specified for OAuthSimple.setURL'); 154 | } 155 | this._path = path; 156 | return this; 157 | }; 158 | 159 | /** convienence method for setURL 160 | * 161 | * @param path {string} see .setURL 162 | */ 163 | this.setPath = function(path){ 164 | return this.setURL(path); 165 | }; 166 | 167 | /** set the "action" for the url, (e.g. GET,POST, DELETE, etc.) 168 | * 169 | * @param action {string} HTTP Action word. 170 | */ 171 | this.setAction = function(action) { 172 | if (action === undefined) { 173 | action="GET"; 174 | } 175 | action = action.toUpperCase(); 176 | if (action.match('[^A-Z]')) { 177 | throw ('Invalid action specified for OAuthSimple.setAction'); 178 | } 179 | this._action = action; 180 | return this; 181 | }; 182 | 183 | /** set the signatures (as well as validate the ones you have) 184 | * 185 | * @param signatures {object} object/hash of the token/signature pairs {api_key:, shared_secret:, oauth_token: oauth_secret:} 186 | */ 187 | this.setTokensAndSecrets = function(signatures) { 188 | if (signatures) 189 | { 190 | for (var i in signatures) { 191 | this._secrets[i] = signatures[i]; 192 | } 193 | } 194 | // Aliases 195 | if (this._secrets['api_key']) { 196 | this._secrets.consumer_key = this._secrets.api_key; 197 | } 198 | if (this._secrets['access_token']) { 199 | this._secrets.oauth_token = this._secrets.access_token; 200 | } 201 | if (this._secrets['access_secret']) { 202 | this._secrets.oauth_secret = this._secrets.access_secret; 203 | } 204 | // Gauntlet 205 | if (this._secrets.consumer_key === undefined) { 206 | throw('Missing required consumer_key in OAuthSimple.setTokensAndSecrets'); 207 | } 208 | if (this._secrets.shared_secret === undefined) { 209 | throw('Missing required shared_secret in OAuthSimple.setTokensAndSecrets'); 210 | } 211 | if ((this._secrets.oauth_token !== undefined) && (this._secrets.oauth_secret === undefined)) { 212 | throw('Missing oauth_secret for supplied oauth_token in OAuthSimple.setTokensAndSecrets'); 213 | } 214 | return this; 215 | }; 216 | 217 | /** set the signature method (currently only Plaintext or SHA-MAC1) 218 | * 219 | * @param method {string} Method of signing the transaction (only PLAINTEXT and SHA-MAC1 allowed for now) 220 | */ 221 | this.setSignatureMethod = function(method) { 222 | if (method === undefined) { 223 | method = this._default_signature_method; 224 | } 225 | //TODO: accept things other than PlainText or SHA-MAC1 226 | if (method.toUpperCase().match(/(PLAINTEXT|HMAC-SHA1)/) === undefined) { 227 | throw ('Unknown signing method specified for OAuthSimple.setSignatureMethod'); 228 | } 229 | this._parameters['oauth_signature_method']= method.toUpperCase(); 230 | return this; 231 | }; 232 | 233 | /** sign the request 234 | * 235 | * note: all arguments are optional, provided you've set them using the 236 | * other helper functions. 237 | * 238 | * @param args {object} hash of arguments for the call 239 | * {action:, path:, parameters:, method:, signatures:} 240 | * all arguments are optional. 241 | */ 242 | this.sign = function (args) { 243 | if (args === undefined) { 244 | args = {}; 245 | } 246 | // Set any given parameters 247 | if(args['action'] !== undefined) { 248 | this.setAction(args['action']); 249 | } 250 | if (args['path'] !== undefined) { 251 | this.setPath(args['path']); 252 | } 253 | if (args['method'] !== undefined) { 254 | this.setSignatureMethod(args['method']); 255 | } 256 | this.setTokensAndSecrets(args['signatures']); 257 | if (args['parameters'] !== undefined){ 258 | this.setParameters(args['parameters']); 259 | } 260 | // check the parameters 261 | var normParams = this._normalizedParameters(); 262 | this._parameters['oauth_signature']=this._generateSignature(normParams); 263 | return { 264 | parameters: this._parameters, 265 | signature: this._oauthEscape(this._parameters['oauth_signature']), 266 | signed_url: this._path + '?' + this._normalizedParameters(), 267 | header: this.getHeaderString() 268 | }; 269 | }; 270 | 271 | /** Return a formatted "header" string 272 | * 273 | * NOTE: This doesn't set the "Authorization: " prefix, which is required. 274 | * I don't set it because various set header functions prefer different 275 | * ways to do that. 276 | * 277 | * @param args {object} see .sign 278 | */ 279 | this.getHeaderString = function(args) { 280 | if (this._parameters['oauth_signature'] === undefined) { 281 | this.sign(args); 282 | } 283 | 284 | var result = 'OAuth '; 285 | for (var pName in this._parameters) 286 | { 287 | if (!pName.match(/^oauth/)) { 288 | continue; 289 | } 290 | if ((this._parameters[pName]) instanceof Array) 291 | { 292 | var pLength = this._parameters[pName].length; 293 | for (var j=0;j>16)+(y>>16)+(l>>16);return(m<<16)|(l&0xFFFF);}function _r(n,c){return(n<>>(32-c));}function _c(x,l){x[l>>5]|=0x80<<(24-l%32);x[((l+64>>9)<<4)+15]=l;var w=[80],a=1732584193,b=-271733879,c=-1732584194,d=271733878,e=-1009589776;for(var i=0;i>5]|=(s.charCodeAt(i/8)&m)<<(32-_z-i%32);}return b;}function _h(k,d){var b=_b(k);if(b.length>16){b=_c(b,k.length*_z);}var p=[16],o=[16];for(var i=0;i<16;i++){p[i]=b[i]^0x36363636;o[i]=b[i]^0x5C5C5C5C;}var h=_c(p.concat(_b(d)),512+d.length*_z);return _c(o.concat(h),512+160);}function _n(b){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s='';for(var i=0;i>2]>>8*(3-i%4))&0xFF)<<16)|(((b[i+1>>2]>>8*(3-(i+1)%4))&0xFF)<<8)|((b[i+2>>2]>>8*(3-(i+2)%4))&0xFF);for(var j=0;j<4;j++){if(i*8+j*6>b.length*32){s+=_p;}else{s+=t.charAt((r>>6*(3-j))&0x3F);}}}return s;}function _x(k,d){return _n(_h(k,d));}return _x(k,d); 398 | } 399 | 400 | 401 | this._normalizedParameters = function() { 402 | var elements = new Array(); 403 | var paramNames = []; 404 | var ra =0; 405 | for (var paramName in this._parameters) 406 | { 407 | if (ra++ > 1000) { 408 | throw('runaway 1'); 409 | } 410 | paramNames.unshift(paramName); 411 | } 412 | paramNames = paramNames.sort(); 413 | pLen = paramNames.length; 414 | for (var i=0;i 1000) { 427 | throw('runaway 1'); 428 | } 429 | elements.push(this._oauthEscape(paramName) + '=' + 430 | this._oauthEscape(sorted[j])); 431 | } 432 | continue; 433 | } 434 | elements.push(this._oauthEscape(paramName) + '=' + 435 | this._oauthEscape(this._parameters[paramName])); 436 | } 437 | return elements.join('&'); 438 | }; 439 | 440 | this._generateSignature = function() { 441 | 442 | var secretKey = this._oauthEscape(this._secrets.shared_secret)+'&'+ 443 | this._oauthEscape(this._secrets.oauth_secret); 444 | if (this._parameters['oauth_signature_method'] == 'PLAINTEXT') 445 | { 446 | return secretKey; 447 | } 448 | if (this._parameters['oauth_signature_method'] == 'HMAC-SHA1') 449 | { 450 | var sigString = this._oauthEscape(this._action)+'&'+this._oauthEscape(this._path)+'&'+this._oauthEscape(this._normalizedParameters()); 451 | return this.b64_hmac_sha1(secretKey,sigString); 452 | } 453 | return null; 454 | }; 455 | 456 | return this; 457 | }; 458 | } 459 | -------------------------------------------------------------------------------- /chrome_ex_oauth.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | /** 6 | * Constructor - no need to invoke directly, call initBackgroundPage instead. 7 | * @constructor 8 | * @param {String} url_request_token The OAuth request token URL. 9 | * @param {String} url_auth_token The OAuth authorize token URL. 10 | * @param {String} url_access_token The OAuth access token URL. 11 | * @param {String} consumer_key The OAuth consumer key. 12 | * @param {String} consumer_secret The OAuth consumer secret. 13 | * @param {String} oauth_scope The OAuth scope parameter. 14 | * @param {Object} opt_args Optional arguments. Recognized parameters: 15 | * "app_name" {String} Name of the current application 16 | * "callback_page" {String} If you renamed chrome_ex_oauth.html, the name 17 | * this file was renamed to. 18 | */ 19 | function ChromeExOAuth(url_request_token, url_auth_token, url_access_token, 20 | consumer_key, consumer_secret, oauth_scope, opt_args) { 21 | this.url_request_token = url_request_token; 22 | this.url_auth_token = url_auth_token; 23 | this.url_access_token = url_access_token; 24 | this.consumer_key = consumer_key; 25 | this.consumer_secret = consumer_secret; 26 | this.oauth_scope = oauth_scope; 27 | this.app_name = opt_args && opt_args['app_name'] || 28 | "ChromeExOAuth Library"; 29 | this.key_token = "oauth_token"; 30 | this.key_token_secret = "oauth_token_secret"; 31 | this.callback_page = opt_args && opt_args['callback_page'] || 32 | "chrome_ex_oauth.html"; 33 | this.auth_params = {}; 34 | if (opt_args && opt_args['auth_params']) { 35 | for (key in opt_args['auth_params']) { 36 | if (opt_args['auth_params'].hasOwnProperty(key)) { 37 | this.auth_params[key] = opt_args['auth_params'][key]; 38 | } 39 | } 40 | } 41 | }; 42 | 43 | /******************************************************************************* 44 | * PUBLIC API METHODS 45 | * Call these from your background page. 46 | ******************************************************************************/ 47 | 48 | /** 49 | * Initializes the OAuth helper from the background page. You must call this 50 | * before attempting to make any OAuth calls. 51 | * @param {Object} oauth_config Configuration parameters in a JavaScript object. 52 | * The following parameters are recognized: 53 | * "request_url" {String} OAuth request token URL. 54 | * "authorize_url" {String} OAuth authorize token URL. 55 | * "access_url" {String} OAuth access token URL. 56 | * "consumer_key" {String} OAuth consumer key. 57 | * "consumer_secret" {String} OAuth consumer secret. 58 | * "scope" {String} OAuth access scope. 59 | * "app_name" {String} Application name. 60 | * "auth_params" {Object} Additional parameters to pass to the 61 | * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': 62 | * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth 63 | * @return {ChromeExOAuth} An initialized ChromeExOAuth object. 64 | */ 65 | ChromeExOAuth.initBackgroundPage = function(oauth_config) { 66 | window.chromeExOAuthConfig = oauth_config; 67 | window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config); 68 | window.chromeExOAuthRedirectStarted = false; 69 | window.chromeExOAuthRequestingAccess = false; 70 | 71 | var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page); 72 | var tabs = {}; 73 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { 74 | if (changeInfo.url && 75 | changeInfo.url.substr(0, url_match.length) === url_match && 76 | changeInfo.url != tabs[tabId] && 77 | window.chromeExOAuthRequestingAccess == false) { 78 | chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) { 79 | tabs[tab.id] = tab.url; 80 | chrome.tabs.remove(tabId); 81 | }); 82 | } 83 | }); 84 | 85 | return window.chromeExOAuth; 86 | }; 87 | 88 | /** 89 | * Authorizes the current user with the configued API. You must call this 90 | * before calling sendSignedRequest. 91 | * @param {Function} callback A function to call once an access token has 92 | * been obtained. This callback will be passed the following arguments: 93 | * token {String} The OAuth access token. 94 | * secret {String} The OAuth access token secret. 95 | */ 96 | ChromeExOAuth.prototype.authorize = function(callback) { 97 | if (this.hasToken()) { 98 | callback(this.getToken(), this.getTokenSecret()); 99 | } else { 100 | window.chromeExOAuthOnAuthorize = function(token, secret) { 101 | callback(token, secret); 102 | }; 103 | chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) }); 104 | } 105 | }; 106 | 107 | /** 108 | * Clears any OAuth tokens stored for this configuration. Effectively a 109 | * "logout" of the configured OAuth API. 110 | */ 111 | ChromeExOAuth.prototype.clearTokens = function() { 112 | delete localStorage[this.key_token + encodeURI(this.oauth_scope)]; 113 | delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; 114 | }; 115 | 116 | /** 117 | * Returns whether a token is currently stored for this configuration. 118 | * Effectively a check to see whether the current user is "logged in" to 119 | * the configured OAuth API. 120 | * @return {Boolean} True if an access token exists. 121 | */ 122 | ChromeExOAuth.prototype.hasToken = function() { 123 | return !!this.getToken(); 124 | }; 125 | 126 | /** 127 | * Makes an OAuth-signed HTTP request with the currently authorized tokens. 128 | * @param {String} url The URL to send the request to. Querystring parameters 129 | * should be omitted. 130 | * @param {Function} callback A function to be called once the request is 131 | * completed. This callback will be passed the following arguments: 132 | * responseText {String} The text response. 133 | * xhr {XMLHttpRequest} The XMLHttpRequest object which was used to 134 | * send the request. Useful if you need to check response status 135 | * code, etc. 136 | * @param {Object} opt_params Additional parameters to configure the request. 137 | * The following parameters are accepted: 138 | * "method" {String} The HTTP method to use. Defaults to "GET". 139 | * "body" {String} A request body to send. Defaults to null. 140 | * "parameters" {Object} Query parameters to include in the request. 141 | * "headers" {Object} Additional headers to include in the request. 142 | */ 143 | ChromeExOAuth.prototype.sendSignedRequest = function(url, callback, 144 | opt_params) { 145 | var method = opt_params && opt_params['method'] || 'GET'; 146 | var body = opt_params && opt_params['body'] || null; 147 | var params = opt_params && opt_params['parameters'] || {}; 148 | var headers = opt_params && opt_params['headers'] || {}; 149 | 150 | var signedUrl = this.signURL(url, method, params); 151 | 152 | ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) { 153 | if (xhr.readyState == 4) { 154 | callback(xhr.responseText, xhr); 155 | } 156 | }); 157 | }; 158 | 159 | /** 160 | * Adds the required OAuth parameters to the given url and returns the 161 | * result. Useful if you need a signed url but don't want to make an XHR 162 | * request. 163 | * @param {String} method The http method to use. 164 | * @param {String} url The base url of the resource you are querying. 165 | * @param {Object} opt_params Query parameters to include in the request. 166 | * @return {String} The base url plus any query params plus any OAuth params. 167 | */ 168 | ChromeExOAuth.prototype.signURL = function(url, method, opt_params) { 169 | var token = this.getToken(); 170 | var secret = this.getTokenSecret(); 171 | if (!token || !secret) { 172 | throw new Error("No oauth token or token secret"); 173 | } 174 | 175 | var params = opt_params || {}; 176 | 177 | var result = OAuthSimple().sign({ 178 | action : method, 179 | path : url, 180 | parameters : params, 181 | signatures: { 182 | consumer_key : this.consumer_key, 183 | shared_secret : this.consumer_secret, 184 | oauth_secret : secret, 185 | oauth_token: token 186 | } 187 | }); 188 | 189 | return result.signed_url; 190 | }; 191 | 192 | /** 193 | * Generates the Authorization header based on the oauth parameters. 194 | * @param {String} url The base url of the resource you are querying. 195 | * @param {Object} opt_params Query parameters to include in the request. 196 | * @return {String} An Authorization header containing the oauth_* params. 197 | */ 198 | ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method, 199 | opt_params) { 200 | var token = this.getToken(); 201 | var secret = this.getTokenSecret(); 202 | if (!token || !secret) { 203 | throw new Error("No oauth token or token secret"); 204 | } 205 | 206 | var params = opt_params || {}; 207 | 208 | return OAuthSimple().getHeaderString({ 209 | action: method, 210 | path : url, 211 | parameters : params, 212 | signatures: { 213 | consumer_key : this.consumer_key, 214 | shared_secret : this.consumer_secret, 215 | oauth_secret : secret, 216 | oauth_token: token 217 | } 218 | }); 219 | }; 220 | 221 | /******************************************************************************* 222 | * PRIVATE API METHODS 223 | * Used by the library. There should be no need to call these methods directly. 224 | ******************************************************************************/ 225 | 226 | /** 227 | * Creates a new ChromeExOAuth object from the supplied configuration object. 228 | * @param {Object} oauth_config Configuration parameters in a JavaScript object. 229 | * The following parameters are recognized: 230 | * "request_url" {String} OAuth request token URL. 231 | * "authorize_url" {String} OAuth authorize token URL. 232 | * "access_url" {String} OAuth access token URL. 233 | * "consumer_key" {String} OAuth consumer key. 234 | * "consumer_secret" {String} OAuth consumer secret. 235 | * "scope" {String} OAuth access scope. 236 | * "app_name" {String} Application name. 237 | * "auth_params" {Object} Additional parameters to pass to the 238 | * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': 239 | * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth 240 | * @return {ChromeExOAuth} An initialized ChromeExOAuth object. 241 | */ 242 | ChromeExOAuth.fromConfig = function(oauth_config) { 243 | return new ChromeExOAuth( 244 | oauth_config['request_url'], 245 | oauth_config['authorize_url'], 246 | oauth_config['access_url'], 247 | oauth_config['consumer_key'], 248 | oauth_config['consumer_secret'], 249 | oauth_config['scope'], 250 | { 251 | 'app_name' : oauth_config['app_name'], 252 | 'auth_params' : oauth_config['auth_params'] 253 | } 254 | ); 255 | }; 256 | 257 | /** 258 | * Initializes chrome_ex_oauth.html and redirects the page if needed to start 259 | * the OAuth flow. Once an access token is obtained, this function closes 260 | * chrome_ex_oauth.html. 261 | */ 262 | ChromeExOAuth.initCallbackPage = function() { 263 | var background_page = chrome.extension.getBackgroundPage(); 264 | var oauth_config = background_page.chromeExOAuthConfig; 265 | var oauth = ChromeExOAuth.fromConfig(oauth_config); 266 | background_page.chromeExOAuthRedirectStarted = true; 267 | oauth.initOAuthFlow(function (token, secret) { 268 | background_page.chromeExOAuthOnAuthorize(token, secret); 269 | background_page.chromeExOAuthRedirectStarted = false; 270 | chrome.tabs.query({active: true, currentWindow: true}, function (tabs) { 271 | chrome.tabs.remove(tabs[0].id); 272 | }); 273 | }); 274 | }; 275 | 276 | /** 277 | * Sends an HTTP request. Convenience wrapper for XMLHttpRequest calls. 278 | * @param {String} method The HTTP method to use. 279 | * @param {String} url The URL to send the request to. 280 | * @param {Object} headers Optional request headers in key/value format. 281 | * @param {String} body Optional body content. 282 | * @param {Function} callback Function to call when the XMLHttpRequest's 283 | * ready state changes. See documentation for XMLHttpRequest's 284 | * onreadystatechange handler for more information. 285 | */ 286 | ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) { 287 | var xhr = new XMLHttpRequest(); 288 | xhr.onreadystatechange = function(data) { 289 | callback(xhr, data); 290 | } 291 | xhr.open(method, url, true); 292 | if (headers) { 293 | for (var header in headers) { 294 | if (headers.hasOwnProperty(header)) { 295 | xhr.setRequestHeader(header, headers[header]); 296 | } 297 | } 298 | } 299 | xhr.send(body); 300 | }; 301 | 302 | /** 303 | * Decodes a URL-encoded string into key/value pairs. 304 | * @param {String} encoded An URL-encoded string. 305 | * @return {Object} An object representing the decoded key/value pairs found 306 | * in the encoded string. 307 | */ 308 | ChromeExOAuth.formDecode = function(encoded) { 309 | var params = encoded.split("&"); 310 | var decoded = {}; 311 | for (var i = 0, param; param = params[i]; i++) { 312 | var keyval = param.split("="); 313 | if (keyval.length == 2) { 314 | var key = ChromeExOAuth.fromRfc3986(keyval[0]); 315 | var val = ChromeExOAuth.fromRfc3986(keyval[1]); 316 | decoded[key] = val; 317 | } 318 | } 319 | return decoded; 320 | }; 321 | 322 | /** 323 | * Returns the current window's querystring decoded into key/value pairs. 324 | * @return {Object} A object representing any key/value pairs found in the 325 | * current window's querystring. 326 | */ 327 | ChromeExOAuth.getQueryStringParams = function() { 328 | var urlparts = window.location.href.split("?"); 329 | if (urlparts.length >= 2) { 330 | var querystring = urlparts.slice(1).join("?"); 331 | return ChromeExOAuth.formDecode(querystring); 332 | } 333 | return {}; 334 | }; 335 | 336 | /** 337 | * Binds a function call to a specific object. This function will also take 338 | * a variable number of additional arguments which will be prepended to the 339 | * arguments passed to the bound function when it is called. 340 | * @param {Function} func The function to bind. 341 | * @param {Object} obj The object to bind to the function's "this". 342 | * @return {Function} A closure that will call the bound function. 343 | */ 344 | ChromeExOAuth.bind = function(func, obj) { 345 | var newargs = Array.prototype.slice.call(arguments).slice(2); 346 | return function() { 347 | var combinedargs = newargs.concat(Array.prototype.slice.call(arguments)); 348 | func.apply(obj, combinedargs); 349 | }; 350 | }; 351 | 352 | /** 353 | * Encodes a value according to the RFC3986 specification. 354 | * @param {String} val The string to encode. 355 | */ 356 | ChromeExOAuth.toRfc3986 = function(val){ 357 | return encodeURIComponent(val) 358 | .replace(/\!/g, "%21") 359 | .replace(/\*/g, "%2A") 360 | .replace(/'/g, "%27") 361 | .replace(/\(/g, "%28") 362 | .replace(/\)/g, "%29"); 363 | }; 364 | 365 | /** 366 | * Decodes a string that has been encoded according to RFC3986. 367 | * @param {String} val The string to decode. 368 | */ 369 | ChromeExOAuth.fromRfc3986 = function(val){ 370 | var tmp = val 371 | .replace(/%21/g, "!") 372 | .replace(/%2A/g, "*") 373 | .replace(/%27/g, "'") 374 | .replace(/%28/g, "(") 375 | .replace(/%29/g, ")"); 376 | return decodeURIComponent(tmp); 377 | }; 378 | 379 | /** 380 | * Adds a key/value parameter to the supplied URL. 381 | * @param {String} url An URL which may or may not contain querystring values. 382 | * @param {String} key A key 383 | * @param {String} value A value 384 | * @return {String} The URL with URL-encoded versions of the key and value 385 | * appended, prefixing them with "&" or "?" as needed. 386 | */ 387 | ChromeExOAuth.addURLParam = function(url, key, value) { 388 | var sep = (url.indexOf('?') >= 0) ? "&" : "?"; 389 | return url + sep + 390 | ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value); 391 | }; 392 | 393 | /** 394 | * Stores an OAuth token for the configured scope. 395 | * @param {String} token The token to store. 396 | */ 397 | ChromeExOAuth.prototype.setToken = function(token) { 398 | localStorage[this.key_token + encodeURI(this.oauth_scope)] = token; 399 | }; 400 | 401 | /** 402 | * Retrieves any stored token for the configured scope. 403 | * @return {String} The stored token. 404 | */ 405 | ChromeExOAuth.prototype.getToken = function() { 406 | return localStorage[this.key_token + encodeURI(this.oauth_scope)]; 407 | }; 408 | 409 | /** 410 | * Stores an OAuth token secret for the configured scope. 411 | * @param {String} secret The secret to store. 412 | */ 413 | ChromeExOAuth.prototype.setTokenSecret = function(secret) { 414 | localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret; 415 | }; 416 | 417 | /** 418 | * Retrieves any stored secret for the configured scope. 419 | * @return {String} The stored secret. 420 | */ 421 | ChromeExOAuth.prototype.getTokenSecret = function() { 422 | return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; 423 | }; 424 | 425 | /** 426 | * Starts an OAuth authorization flow for the current page. If a token exists, 427 | * no redirect is needed and the supplied callback is called immediately. 428 | * If this method detects that a redirect has finished, it grabs the 429 | * appropriate OAuth parameters from the URL and attempts to retrieve an 430 | * access token. If no token exists and no redirect has happened, then 431 | * an access token is requested and the page is ultimately redirected. 432 | * @param {Function} callback The function to call once the flow has finished. 433 | * This callback will be passed the following arguments: 434 | * token {String} The OAuth access token. 435 | * secret {String} The OAuth access token secret. 436 | */ 437 | ChromeExOAuth.prototype.initOAuthFlow = function(callback) { 438 | if (!this.hasToken()) { 439 | var params = ChromeExOAuth.getQueryStringParams(); 440 | if (params['chromeexoauthcallback'] == 'true') { 441 | var oauth_token = params['oauth_token']; 442 | var oauth_verifier = params['oauth_verifier'] 443 | this.getAccessToken(oauth_token, oauth_verifier, callback); 444 | } else { 445 | var request_params = { 446 | 'url_callback_param' : 'chromeexoauthcallback' 447 | } 448 | this.getRequestToken(function(url) { 449 | window.location.href = url; 450 | }, request_params); 451 | } 452 | } else { 453 | callback(this.getToken(), this.getTokenSecret()); 454 | } 455 | }; 456 | 457 | /** 458 | * Requests an OAuth request token. 459 | * @param {Function} callback Function to call once the authorize URL is 460 | * calculated. This callback will be passed the following arguments: 461 | * url {String} The URL the user must be redirected to in order to 462 | * approve the token. 463 | * @param {Object} opt_args Optional arguments. The following parameters 464 | * are accepted: 465 | * "url_callback" {String} The URL the OAuth provider will redirect to. 466 | * "url_callback_param" {String} A parameter to include in the callback 467 | * URL in order to indicate to this library that a redirect has 468 | * taken place. 469 | */ 470 | ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) { 471 | if (typeof callback !== "function") { 472 | throw new Error("Specified callback must be a function."); 473 | } 474 | var url = opt_args && opt_args['url_callback'] || 475 | window && window.top && window.top.location && 476 | window.top.location.href; 477 | 478 | var url_param = opt_args && opt_args['url_callback_param'] || 479 | "chromeexoauthcallback"; 480 | var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true"); 481 | 482 | var result = OAuthSimple().sign({ 483 | path : this.url_request_token, 484 | parameters: { 485 | "xoauth_displayname" : this.app_name, 486 | "scope" : this.oauth_scope, 487 | "oauth_callback" : url_callback 488 | }, 489 | signatures: { 490 | consumer_key : this.consumer_key, 491 | shared_secret : this.consumer_secret 492 | } 493 | }); 494 | var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback); 495 | ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); 496 | }; 497 | 498 | /** 499 | * Called when a request token has been returned. Stores the request token 500 | * secret for later use and sends the authorization url to the supplied 501 | * callback (for redirecting the user). 502 | * @param {Function} callback Function to call once the authorize URL is 503 | * calculated. This callback will be passed the following arguments: 504 | * url {String} The URL the user must be redirected to in order to 505 | * approve the token. 506 | * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the 507 | * request token. 508 | */ 509 | ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) { 510 | if (xhr.readyState == 4) { 511 | if (xhr.status == 200) { 512 | var params = ChromeExOAuth.formDecode(xhr.responseText); 513 | var token = params['oauth_token']; 514 | this.setTokenSecret(params['oauth_token_secret']); 515 | var url = ChromeExOAuth.addURLParam(this.url_auth_token, 516 | "oauth_token", token); 517 | for (var key in this.auth_params) { 518 | if (this.auth_params.hasOwnProperty(key)) { 519 | url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]); 520 | } 521 | } 522 | callback(url); 523 | } else { 524 | throw new Error("Fetching request token failed. Status " + xhr.status); 525 | } 526 | } 527 | }; 528 | 529 | /** 530 | * Requests an OAuth access token. 531 | * @param {String} oauth_token The OAuth request token. 532 | * @param {String} oauth_verifier The OAuth token verifier. 533 | * @param {Function} callback The function to call once the token is obtained. 534 | * This callback will be passed the following arguments: 535 | * token {String} The OAuth access token. 536 | * secret {String} The OAuth access token secret. 537 | */ 538 | ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier, 539 | callback) { 540 | if (typeof callback !== "function") { 541 | throw new Error("Specified callback must be a function."); 542 | } 543 | var bg = chrome.extension.getBackgroundPage(); 544 | if (bg.chromeExOAuthRequestingAccess == false) { 545 | bg.chromeExOAuthRequestingAccess = true; 546 | 547 | var result = OAuthSimple().sign({ 548 | path : this.url_access_token, 549 | parameters: { 550 | "oauth_token" : oauth_token, 551 | "oauth_verifier" : oauth_verifier 552 | }, 553 | signatures: { 554 | consumer_key : this.consumer_key, 555 | shared_secret : this.consumer_secret, 556 | oauth_secret : this.getTokenSecret(this.oauth_scope) 557 | } 558 | }); 559 | 560 | var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback); 561 | ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); 562 | } 563 | }; 564 | 565 | /** 566 | * Called when an access token has been returned. Stores the access token and 567 | * access token secret for later use and sends them to the supplied callback. 568 | * @param {Function} callback The function to call once the token is obtained. 569 | * This callback will be passed the following arguments: 570 | * token {String} The OAuth access token. 571 | * secret {String} The OAuth access token secret. 572 | * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the 573 | * access token. 574 | */ 575 | ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) { 576 | if (xhr.readyState == 4) { 577 | var bg = chrome.extension.getBackgroundPage(); 578 | if (xhr.status == 200) { 579 | var params = ChromeExOAuth.formDecode(xhr.responseText); 580 | var token = params["oauth_token"]; 581 | var secret = params["oauth_token_secret"]; 582 | this.setToken(token); 583 | this.setTokenSecret(secret); 584 | bg.chromeExOAuthRequestingAccess = false; 585 | callback(token, secret); 586 | } else { 587 | bg.chromeExOAuthRequestingAccess = false; 588 | throw new Error("Fetching access token failed with status " + xhr.status); 589 | } 590 | } 591 | }; -------------------------------------------------------------------------------- /twitter-text.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * twitter-text 2.0.0 3 | * 4 | * Copyright 2012 Twitter, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this work except in compliance with the License. 8 | * You may obtain a copy of the License at: 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | */ 12 | (function (global, factory) { 13 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 14 | typeof define === 'function' && define.amd ? define(factory) : 15 | (global.twttr = global.twttr || {}, global.twttr.txt = factory()); 16 | }(this, (function () { 'use strict'; 17 | 18 | var cashtag = /[a-z]{1,6}(?:[._][a-z]{1,2})?/i; 19 | 20 | var punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/; 21 | 22 | // Builds a RegExp 23 | var regexSupplant = function (regex, map, flags) { 24 | flags = flags || ''; 25 | if (typeof regex !== 'string') { 26 | if (regex.global && flags.indexOf('g') < 0) { 27 | flags += 'g'; 28 | } 29 | if (regex.ignoreCase && flags.indexOf('i') < 0) { 30 | flags += 'i'; 31 | } 32 | if (regex.multiline && flags.indexOf('m') < 0) { 33 | flags += 'm'; 34 | } 35 | 36 | regex = regex.source; 37 | } 38 | 39 | return new RegExp(regex.replace(/#\{(\w+)\}/g, function (match, name) { 40 | var newRegex = map[name] || ''; 41 | if (typeof newRegex !== 'string') { 42 | newRegex = newRegex.source; 43 | } 44 | return newRegex; 45 | }), flags); 46 | }; 47 | 48 | var spacesGroup = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/; 49 | 50 | var spaces = regexSupplant(/[#{spacesGroup}]/, { spacesGroup: spacesGroup }); 51 | 52 | var validCashtag = regexSupplant('(^|#{spaces})(\\$)(#{cashtag})(?=$|\\s|[#{punct}])', { cashtag: cashtag, spaces: spaces, punct: punct }, 'gi'); 53 | 54 | var extractCashtagsWithIndices = function (text) { 55 | if (!text || text.indexOf('$') === -1) { 56 | return []; 57 | } 58 | 59 | var tags = []; 60 | 61 | text.replace(validCashtag, function (match, before, dollar, cashtag, offset, chunk) { 62 | var startPosition = offset + before.length; 63 | var endPosition = startPosition + cashtag.length + 1; 64 | tags.push({ 65 | cashtag: cashtag, 66 | indices: [startPosition, endPosition] 67 | }); 68 | }); 69 | 70 | return tags; 71 | }; 72 | 73 | var hashSigns = /[##]/; 74 | 75 | var endHashtagMatch = regexSupplant(/^(?:#{hashSigns}|:\/\/)/, { hashSigns: hashSigns }); 76 | 77 | var 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@]|$))')); 78 | 79 | var invalidCharsGroup = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/; 80 | 81 | // simple string interpolation 82 | var stringSupplant = function (str, map) { 83 | return str.replace(/#\{(\w+)\}/g, function (match, name) { 84 | return map[name] || ''; 85 | }); 86 | }; 87 | 88 | var invalidDomainChars = stringSupplant('#{punct}#{spacesGroup}#{invalidCharsGroup}', { punct: punct, spacesGroup: spacesGroup, invalidCharsGroup: invalidCharsGroup }); 89 | 90 | var validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/, { invalidDomainChars: invalidDomainChars }); 91 | 92 | var validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/, { validDomainChars: validDomainChars }); 93 | 94 | var 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@]|$))')); 95 | 96 | var validPunycode = /(?:xn--[\-0-9a-z]+)/; 97 | 98 | var validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/, { validDomainChars: validDomainChars }); 99 | 100 | var validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/, { validDomainName: validDomainName, validSubdomain: validSubdomain, validGTLD: validGTLD, validCCTLD: validCCTLD, validPunycode: validPunycode }); 101 | 102 | var validPortNumber = /[0-9]+/; 103 | 104 | var cyrillicLettersAndMarks = /\u0400-\u04FF/; 105 | 106 | var 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/; 107 | 108 | var validGeneralUrlPathChars = regexSupplant(/[a-z#{cyrillicLettersAndMarks}0-9!\*';:=\+,\.\$\/%#\[\]\-\u2013_~@\|&#{latinAccentChars}]/i, { cyrillicLettersAndMarks: cyrillicLettersAndMarks, latinAccentChars: latinAccentChars }); 109 | 110 | // Allow URL paths to contain up to two nested levels of balanced parens 111 | // 1. Used in Wikipedia URLs like /Primer_(film) 112 | // 2. Used in IIS sessions like /S(dfd346)/ 113 | // 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/ 114 | var validUrlBalancedParens = regexSupplant('\\(' + '(?:' + '#{validGeneralUrlPathChars}+' + '|' + 115 | // allow one nested level of balanced parentheses 116 | '(?:' + '#{validGeneralUrlPathChars}*' + '\\(' + '#{validGeneralUrlPathChars}+' + '\\)' + '#{validGeneralUrlPathChars}*' + ')' + ')' + '\\)', { validGeneralUrlPathChars: validGeneralUrlPathChars }, 'i'); 117 | 118 | // Valid end-of-path chracters (so /foo. does not gobble the period). 119 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 120 | var validUrlPathEndingChars = regexSupplant(/[\+\-a-z#{cyrillicLettersAndMarks}0-9=_#\/#{latinAccentChars}]|(?:#{validUrlBalancedParens})/i, { cyrillicLettersAndMarks: cyrillicLettersAndMarks, latinAccentChars: latinAccentChars, validUrlBalancedParens: validUrlBalancedParens }); 121 | 122 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/ 123 | var validUrlPath = regexSupplant('(?:' + '(?:' + '#{validGeneralUrlPathChars}*' + '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' + '#{validUrlPathEndingChars}' + ')|(?:@#{validGeneralUrlPathChars}+\/)' + ')', { 124 | validGeneralUrlPathChars: validGeneralUrlPathChars, 125 | validUrlBalancedParens: validUrlBalancedParens, 126 | validUrlPathEndingChars: validUrlPathEndingChars 127 | }, 'i'); 128 | 129 | var validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalidCharsGroup}]|^)/, { invalidCharsGroup: invalidCharsGroup }); 130 | 131 | var validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i; 132 | 133 | var validUrlQueryEndingChars = /[a-z0-9\-_&=#\/]/i; 134 | 135 | var extractUrl = regexSupplant('(' + // $1 total match 136 | '(#{validUrlPrecedingChars})' + // $2 Preceeding chracter 137 | '(' + // $3 URL 138 | '(https?:\\/\\/)?' + // $4 Protocol (optional) 139 | '(#{validDomain})' + // $5 Domain(s) 140 | '(?::(#{validPortNumber}))?' + // $6 Port number (optional) 141 | '(\\/#{validUrlPath}*)?' + // $7 URL Path 142 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $8 Query String 143 | ')' + ')', { validUrlPrecedingChars: validUrlPrecedingChars, validDomain: validDomain, validPortNumber: validPortNumber, validUrlPath: validUrlPath, validUrlQueryChars: validUrlQueryChars, validUrlQueryEndingChars: validUrlQueryEndingChars }, 'gi'); 144 | 145 | var invalidShortDomain = regexSupplant(/^#{validDomainName}#{validCCTLD}$/i, { validDomainName: validDomainName, validCCTLD: validCCTLD }); 146 | 147 | var invalidUrlWithoutProtocolPrecedingChars = /[-_.\/]$/; 148 | 149 | var asyncGenerator = function () { 150 | function AwaitValue(value) { 151 | this.value = value; 152 | } 153 | 154 | function AsyncGenerator(gen) { 155 | var front, back; 156 | 157 | function send(key, arg) { 158 | return new Promise(function (resolve, reject) { 159 | var request = { 160 | key: key, 161 | arg: arg, 162 | resolve: resolve, 163 | reject: reject, 164 | next: null 165 | }; 166 | 167 | if (back) { 168 | back = back.next = request; 169 | } else { 170 | front = back = request; 171 | resume(key, arg); 172 | } 173 | }); 174 | } 175 | 176 | function resume(key, arg) { 177 | try { 178 | var result = gen[key](arg); 179 | var value = result.value; 180 | 181 | if (value instanceof AwaitValue) { 182 | Promise.resolve(value.value).then(function (arg) { 183 | resume("next", arg); 184 | }, function (arg) { 185 | resume("throw", arg); 186 | }); 187 | } else { 188 | settle(result.done ? "return" : "normal", result.value); 189 | } 190 | } catch (err) { 191 | settle("throw", err); 192 | } 193 | } 194 | 195 | function settle(type, value) { 196 | switch (type) { 197 | case "return": 198 | front.resolve({ 199 | value: value, 200 | done: true 201 | }); 202 | break; 203 | 204 | case "throw": 205 | front.reject(value); 206 | break; 207 | 208 | default: 209 | front.resolve({ 210 | value: value, 211 | done: false 212 | }); 213 | break; 214 | } 215 | 216 | front = front.next; 217 | 218 | if (front) { 219 | resume(front.key, front.arg); 220 | } else { 221 | back = null; 222 | } 223 | } 224 | 225 | this._invoke = send; 226 | 227 | if (typeof gen.return !== "function") { 228 | this.return = undefined; 229 | } 230 | } 231 | 232 | if (typeof Symbol === "function" && Symbol.asyncIterator) { 233 | AsyncGenerator.prototype[Symbol.asyncIterator] = function () { 234 | return this; 235 | }; 236 | } 237 | 238 | AsyncGenerator.prototype.next = function (arg) { 239 | return this._invoke("next", arg); 240 | }; 241 | 242 | AsyncGenerator.prototype.throw = function (arg) { 243 | return this._invoke("throw", arg); 244 | }; 245 | 246 | AsyncGenerator.prototype.return = function (arg) { 247 | return this._invoke("return", arg); 248 | }; 249 | 250 | return { 251 | wrap: function (fn) { 252 | return function () { 253 | return new AsyncGenerator(fn.apply(this, arguments)); 254 | }; 255 | }, 256 | await: function (value) { 257 | return new AwaitValue(value); 258 | } 259 | }; 260 | }(); 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | var _extends = Object.assign || function (target) { 277 | for (var i = 1; i < arguments.length; i++) { 278 | var source = arguments[i]; 279 | 280 | for (var key in source) { 281 | if (Object.prototype.hasOwnProperty.call(source, key)) { 282 | target[key] = source[key]; 283 | } 284 | } 285 | } 286 | 287 | return target; 288 | }; 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | var toConsumableArray = function (arr) { 325 | if (Array.isArray(arr)) { 326 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 327 | 328 | return arr2; 329 | } else { 330 | return Array.from(arr); 331 | } 332 | }; 333 | 334 | 'use strict'; 335 | 336 | /** Highest positive signed 32-bit float value */ 337 | 338 | var maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1 339 | 340 | /** Bootstring parameters */ 341 | var base = 36; 342 | var tMin = 1; 343 | var tMax = 26; 344 | var skew = 38; 345 | var damp = 700; 346 | var initialBias = 72; 347 | var initialN = 128; // 0x80 348 | var delimiter = '-'; // '\x2D' 349 | 350 | /** Regular expressions */ 351 | var regexPunycode = /^xn--/; 352 | var regexNonASCII = /[^\0-\x7E]/; // non-ASCII chars 353 | var regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators 354 | 355 | /** Error messages */ 356 | var errors = { 357 | 'overflow': 'Overflow: input needs wider integers to process', 358 | 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', 359 | 'invalid-input': 'Invalid input' 360 | }; 361 | 362 | /** Convenience shortcuts */ 363 | var baseMinusTMin = base - tMin; 364 | var floor = Math.floor; 365 | var stringFromCharCode = String.fromCharCode; 366 | 367 | /*--------------------------------------------------------------------------*/ 368 | 369 | /** 370 | * A generic error utility function. 371 | * @private 372 | * @param {String} type The error type. 373 | * @returns {Error} Throws a `RangeError` with the applicable error message. 374 | */ 375 | function error(type) { 376 | throw new RangeError(errors[type]); 377 | } 378 | 379 | /** 380 | * A generic `Array#map` utility function. 381 | * @private 382 | * @param {Array} array The array to iterate over. 383 | * @param {Function} callback The function that gets called for every array 384 | * item. 385 | * @returns {Array} A new array of values returned by the callback function. 386 | */ 387 | function map(array, fn) { 388 | var result = []; 389 | var length = array.length; 390 | while (length--) { 391 | result[length] = fn(array[length]); 392 | } 393 | return result; 394 | } 395 | 396 | /** 397 | * A simple `Array#map`-like wrapper to work with domain name strings or email 398 | * addresses. 399 | * @private 400 | * @param {String} domain The domain name or email address. 401 | * @param {Function} callback The function that gets called for every 402 | * character. 403 | * @returns {Array} A new string of characters returned by the callback 404 | * function. 405 | */ 406 | function mapDomain(string, fn) { 407 | var parts = string.split('@'); 408 | var result = ''; 409 | if (parts.length > 1) { 410 | // In email addresses, only the domain name should be punycoded. Leave 411 | // the local part (i.e. everything up to `@`) intact. 412 | result = parts[0] + '@'; 413 | string = parts[1]; 414 | } 415 | // Avoid `split(regex)` for IE8 compatibility. See #17. 416 | string = string.replace(regexSeparators, '\x2E'); 417 | var labels = string.split('.'); 418 | var encoded = map(labels, fn).join('.'); 419 | return result + encoded; 420 | } 421 | 422 | /** 423 | * Creates an array containing the numeric code points of each Unicode 424 | * character in the string. While JavaScript uses UCS-2 internally, 425 | * this function will convert a pair of surrogate halves (each of which 426 | * UCS-2 exposes as separate characters) into a single code point, 427 | * matching UTF-16. 428 | * @see `punycode.ucs2.encode` 429 | * @see 430 | * @memberOf punycode.ucs2 431 | * @name decode 432 | * @param {String} string The Unicode input string (UCS-2). 433 | * @returns {Array} The new array of code points. 434 | */ 435 | function ucs2decode(string) { 436 | var output = []; 437 | var counter = 0; 438 | var length = string.length; 439 | while (counter < length) { 440 | var value = string.charCodeAt(counter++); 441 | if (value >= 0xD800 && value <= 0xDBFF && counter < length) { 442 | // It's a high surrogate, and there is a next character. 443 | var extra = string.charCodeAt(counter++); 444 | if ((extra & 0xFC00) == 0xDC00) { 445 | // Low surrogate. 446 | output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); 447 | } else { 448 | // It's an unmatched surrogate; only append this code unit, in case the 449 | // next code unit is the high surrogate of a surrogate pair. 450 | output.push(value); 451 | counter--; 452 | } 453 | } else { 454 | output.push(value); 455 | } 456 | } 457 | return output; 458 | } 459 | 460 | /** 461 | * Creates a string based on an array of numeric code points. 462 | * @see `punycode.ucs2.decode` 463 | * @memberOf punycode.ucs2 464 | * @name encode 465 | * @param {Array} codePoints The array of numeric code points. 466 | * @returns {String} The new Unicode string (UCS-2). 467 | */ 468 | var ucs2encode = function ucs2encode(array) { 469 | return String.fromCodePoint.apply(String, toConsumableArray(array)); 470 | }; 471 | 472 | /** 473 | * Converts a basic code point into a digit/integer. 474 | * @see `digitToBasic()` 475 | * @private 476 | * @param {Number} codePoint The basic numeric code point value. 477 | * @returns {Number} The numeric value of a basic code point (for use in 478 | * representing integers) in the range `0` to `base - 1`, or `base` if 479 | * the code point does not represent a value. 480 | */ 481 | var basicToDigit = function basicToDigit(codePoint) { 482 | if (codePoint - 0x30 < 0x0A) { 483 | return codePoint - 0x16; 484 | } 485 | if (codePoint - 0x41 < 0x1A) { 486 | return codePoint - 0x41; 487 | } 488 | if (codePoint - 0x61 < 0x1A) { 489 | return codePoint - 0x61; 490 | } 491 | return base; 492 | }; 493 | 494 | /** 495 | * Converts a digit/integer into a basic code point. 496 | * @see `basicToDigit()` 497 | * @private 498 | * @param {Number} digit The numeric value of a basic code point. 499 | * @returns {Number} The basic code point whose value (when used for 500 | * representing integers) is `digit`, which needs to be in the range 501 | * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is 502 | * used; else, the lowercase form is used. The behavior is undefined 503 | * if `flag` is non-zero and `digit` has no uppercase form. 504 | */ 505 | var digitToBasic = function digitToBasic(digit, flag) { 506 | // 0..25 map to ASCII a..z or A..Z 507 | // 26..35 map to ASCII 0..9 508 | return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); 509 | }; 510 | 511 | /** 512 | * Bias adaptation function as per section 3.4 of RFC 3492. 513 | * https://tools.ietf.org/html/rfc3492#section-3.4 514 | * @private 515 | */ 516 | var adapt = function adapt(delta, numPoints, firstTime) { 517 | var k = 0; 518 | delta = firstTime ? floor(delta / damp) : delta >> 1; 519 | delta += floor(delta / numPoints); 520 | for (; /* no initialization */delta > baseMinusTMin * tMax >> 1; k += base) { 521 | delta = floor(delta / baseMinusTMin); 522 | } 523 | return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); 524 | }; 525 | 526 | /** 527 | * Converts a Punycode string of ASCII-only symbols to a string of Unicode 528 | * symbols. 529 | * @memberOf punycode 530 | * @param {String} input The Punycode string of ASCII-only symbols. 531 | * @returns {String} The resulting string of Unicode symbols. 532 | */ 533 | var decode = function decode(input) { 534 | // Don't use UCS-2. 535 | var output = []; 536 | var inputLength = input.length; 537 | var i = 0; 538 | var n = initialN; 539 | var bias = initialBias; 540 | 541 | // Handle the basic code points: let `basic` be the number of input code 542 | // points before the last delimiter, or `0` if there is none, then copy 543 | // the first basic code points to the output. 544 | 545 | var basic = input.lastIndexOf(delimiter); 546 | if (basic < 0) { 547 | basic = 0; 548 | } 549 | 550 | for (var j = 0; j < basic; ++j) { 551 | // if it's not a basic code point 552 | if (input.charCodeAt(j) >= 0x80) { 553 | error('not-basic'); 554 | } 555 | output.push(input.charCodeAt(j)); 556 | } 557 | 558 | // Main decoding loop: start just after the last delimiter if any basic code 559 | // points were copied; start at the beginning otherwise. 560 | 561 | for (var index = basic > 0 ? basic + 1 : 0; index < inputLength;) /* no final expression */{ 562 | 563 | // `index` is the index of the next character to be consumed. 564 | // Decode a generalized variable-length integer into `delta`, 565 | // which gets added to `i`. The overflow checking is easier 566 | // if we increase `i` as we go, then subtract off its starting 567 | // value at the end to obtain `delta`. 568 | var oldi = i; 569 | for (var w = 1, k = base;; /* no condition */k += base) { 570 | 571 | if (index >= inputLength) { 572 | error('invalid-input'); 573 | } 574 | 575 | var digit = basicToDigit(input.charCodeAt(index++)); 576 | 577 | if (digit >= base || digit > floor((maxInt - i) / w)) { 578 | error('overflow'); 579 | } 580 | 581 | i += digit * w; 582 | var t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; 583 | 584 | if (digit < t) { 585 | break; 586 | } 587 | 588 | var baseMinusT = base - t; 589 | if (w > floor(maxInt / baseMinusT)) { 590 | error('overflow'); 591 | } 592 | 593 | w *= baseMinusT; 594 | } 595 | 596 | var out = output.length + 1; 597 | bias = adapt(i - oldi, out, oldi == 0); 598 | 599 | // `i` was supposed to wrap around from `out` to `0`, 600 | // incrementing `n` each time, so we'll fix that now: 601 | if (floor(i / out) > maxInt - n) { 602 | error('overflow'); 603 | } 604 | 605 | n += floor(i / out); 606 | i %= out; 607 | 608 | // Insert `n` at position `i` of the output. 609 | output.splice(i++, 0, n); 610 | } 611 | 612 | return String.fromCodePoint.apply(String, output); 613 | }; 614 | 615 | /** 616 | * Converts a string of Unicode symbols (e.g. a domain name label) to a 617 | * Punycode string of ASCII-only symbols. 618 | * @memberOf punycode 619 | * @param {String} input The string of Unicode symbols. 620 | * @returns {String} The resulting Punycode string of ASCII-only symbols. 621 | */ 622 | var encode = function encode(input) { 623 | var output = []; 624 | 625 | // Convert the input in UCS-2 to an array of Unicode code points. 626 | input = ucs2decode(input); 627 | 628 | // Cache the length. 629 | var inputLength = input.length; 630 | 631 | // Initialize the state. 632 | var n = initialN; 633 | var delta = 0; 634 | var bias = initialBias; 635 | 636 | // Handle the basic code points. 637 | var _iteratorNormalCompletion = true; 638 | var _didIteratorError = false; 639 | var _iteratorError = undefined; 640 | 641 | try { 642 | for (var _iterator = input[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 643 | var _currentValue2 = _step.value; 644 | 645 | if (_currentValue2 < 0x80) { 646 | output.push(stringFromCharCode(_currentValue2)); 647 | } 648 | } 649 | } catch (err) { 650 | _didIteratorError = true; 651 | _iteratorError = err; 652 | } finally { 653 | try { 654 | if (!_iteratorNormalCompletion && _iterator.return) { 655 | _iterator.return(); 656 | } 657 | } finally { 658 | if (_didIteratorError) { 659 | throw _iteratorError; 660 | } 661 | } 662 | } 663 | 664 | var basicLength = output.length; 665 | var handledCPCount = basicLength; 666 | 667 | // `handledCPCount` is the number of code points that have been handled; 668 | // `basicLength` is the number of basic code points. 669 | 670 | // Finish the basic string with a delimiter unless it's empty. 671 | if (basicLength) { 672 | output.push(delimiter); 673 | } 674 | 675 | // Main encoding loop: 676 | while (handledCPCount < inputLength) { 677 | 678 | // All non-basic code points < n have been handled already. Find the next 679 | // larger one: 680 | var m = maxInt; 681 | var _iteratorNormalCompletion2 = true; 682 | var _didIteratorError2 = false; 683 | var _iteratorError2 = undefined; 684 | 685 | try { 686 | for (var _iterator2 = input[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 687 | var currentValue = _step2.value; 688 | 689 | if (currentValue >= n && currentValue < m) { 690 | m = currentValue; 691 | } 692 | } 693 | 694 | // Increase `delta` enough to advance the decoder's state to , 695 | // but guard against overflow. 696 | } catch (err) { 697 | _didIteratorError2 = true; 698 | _iteratorError2 = err; 699 | } finally { 700 | try { 701 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 702 | _iterator2.return(); 703 | } 704 | } finally { 705 | if (_didIteratorError2) { 706 | throw _iteratorError2; 707 | } 708 | } 709 | } 710 | 711 | var handledCPCountPlusOne = handledCPCount + 1; 712 | if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { 713 | error('overflow'); 714 | } 715 | 716 | delta += (m - n) * handledCPCountPlusOne; 717 | n = m; 718 | 719 | var _iteratorNormalCompletion3 = true; 720 | var _didIteratorError3 = false; 721 | var _iteratorError3 = undefined; 722 | 723 | try { 724 | for (var _iterator3 = input[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { 725 | var _currentValue = _step3.value; 726 | 727 | if (_currentValue < n && ++delta > maxInt) { 728 | error('overflow'); 729 | } 730 | if (_currentValue == n) { 731 | // Represent delta as a generalized variable-length integer. 732 | var q = delta; 733 | for (var k = base;; /* no condition */k += base) { 734 | var t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; 735 | if (q < t) { 736 | break; 737 | } 738 | var qMinusT = q - t; 739 | var baseMinusT = base - t; 740 | output.push(stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))); 741 | q = floor(qMinusT / baseMinusT); 742 | } 743 | 744 | output.push(stringFromCharCode(digitToBasic(q, 0))); 745 | bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); 746 | delta = 0; 747 | ++handledCPCount; 748 | } 749 | } 750 | } catch (err) { 751 | _didIteratorError3 = true; 752 | _iteratorError3 = err; 753 | } finally { 754 | try { 755 | if (!_iteratorNormalCompletion3 && _iterator3.return) { 756 | _iterator3.return(); 757 | } 758 | } finally { 759 | if (_didIteratorError3) { 760 | throw _iteratorError3; 761 | } 762 | } 763 | } 764 | 765 | ++delta; 766 | ++n; 767 | } 768 | return output.join(''); 769 | }; 770 | 771 | /** 772 | * Converts a Punycode string representing a domain name or an email address 773 | * to Unicode. Only the Punycoded parts of the input will be converted, i.e. 774 | * it doesn't matter if you call it on a string that has already been 775 | * converted to Unicode. 776 | * @memberOf punycode 777 | * @param {String} input The Punycoded domain name or email address to 778 | * convert to Unicode. 779 | * @returns {String} The Unicode representation of the given Punycode 780 | * string. 781 | */ 782 | var toUnicode = function toUnicode(input) { 783 | return mapDomain(input, function (string) { 784 | return regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : string; 785 | }); 786 | }; 787 | 788 | /** 789 | * Converts a Unicode string representing a domain name or an email address to 790 | * Punycode. Only the non-ASCII parts of the domain name will be converted, 791 | * i.e. it doesn't matter if you call it with a domain that's already in 792 | * ASCII. 793 | * @memberOf punycode 794 | * @param {String} input The domain name or email address to convert, as a 795 | * Unicode string. 796 | * @returns {String} The Punycode representation of the given domain name or 797 | * email address. 798 | */ 799 | var toASCII = function toASCII(input) { 800 | return mapDomain(input, function (string) { 801 | return regexNonASCII.test(string) ? 'xn--' + encode(string) : string; 802 | }); 803 | }; 804 | 805 | /*--------------------------------------------------------------------------*/ 806 | 807 | /** Define the public API */ 808 | var punycode = { 809 | /** 810 | * A string representing the current Punycode.js version number. 811 | * @memberOf punycode 812 | * @type String 813 | */ 814 | 'version': '2.1.0', 815 | /** 816 | * An object of methods to convert from JavaScript's internal character 817 | * representation (UCS-2) to Unicode code points, and back. 818 | * @see 819 | * @memberOf punycode 820 | * @type Object 821 | */ 822 | 'ucs2': { 823 | 'decode': ucs2decode, 824 | 'encode': ucs2encode 825 | }, 826 | 'decode': decode, 827 | 'encode': encode, 828 | 'toASCII': toASCII, 829 | 'toUnicode': toUnicode 830 | }; 831 | 832 | var punycode_1 = punycode; 833 | 834 | var validAsciiDomain = regexSupplant(/(?:(?:[\-a-z0-9#{latinAccentChars}]+)\.)+(?:#{validGTLD}|#{validCCTLD}|#{validPunycode})/gi, { latinAccentChars: latinAccentChars, validGTLD: validGTLD, validCCTLD: validCCTLD, validPunycode: validPunycode }); 835 | 836 | var MAX_DOMAIN_LABEL_LENGTH = 63; 837 | 838 | // This is an extremely lightweight implementation of domain name validation according to RFC 3490 839 | // Our regexes handle most of the cases well enough 840 | // See https://tools.ietf.org/html/rfc3490#section-4.1 for details 841 | var idna = { 842 | toAscii: function toAscii(domain) { 843 | if (domain.startsWith('xn--') && !domain.match(validAsciiDomain)) { 844 | // Punycode encoded url cannot contain non ASCII characters 845 | return; 846 | } 847 | 848 | var labels = domain.split('.'); 849 | var _iteratorNormalCompletion = true; 850 | var _didIteratorError = false; 851 | var _iteratorError = undefined; 852 | 853 | try { 854 | for (var _iterator = labels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 855 | var label = _step.value; 856 | 857 | var punycodeEncodedLabel = punycode_1.toASCII(label); 858 | if (punycodeEncodedLabel.length < 1 || punycodeEncodedLabel.length > MAX_DOMAIN_LABEL_LENGTH) { 859 | // DNS label has invalid length 860 | return; 861 | } 862 | } 863 | } catch (err) { 864 | _didIteratorError = true; 865 | _iteratorError = err; 866 | } finally { 867 | try { 868 | if (!_iteratorNormalCompletion && _iterator.return) { 869 | _iterator.return(); 870 | } 871 | } finally { 872 | if (_didIteratorError) { 873 | throw _iteratorError; 874 | } 875 | } 876 | } 877 | 878 | return labels.join('.'); 879 | } 880 | }; 881 | 882 | var validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/; 883 | 884 | var validSpecialShortDomain = regexSupplant(/^#{validDomainName}#{validSpecialCCTLD}$/i, { validDomainName: validDomainName, validSpecialCCTLD: validSpecialCCTLD }); 885 | 886 | var validTcoUrl = /^https?:\/\/t\.co\/([a-z0-9]+)/i; 887 | 888 | var DEFAULT_PROTOCOL = 'https://'; 889 | var DEFAULT_PROTOCOL_OPTIONS = { extractUrlsWithoutProtocol: true }; 890 | var MAX_URL_LENGTH = 4096; 891 | var MAX_TCO_SLUG_LENGTH = 40; 892 | 893 | var extractUrlsWithIndices = function extractUrlsWithIndices(text) { 894 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_PROTOCOL_OPTIONS; 895 | 896 | if (!text || (options.extractUrlsWithoutProtocol ? !text.match(/\./) : !text.match(/:/))) { 897 | return []; 898 | } 899 | 900 | var urls = []; 901 | 902 | var _loop = function _loop() { 903 | var before = RegExp.$2; 904 | var url = RegExp.$3; 905 | var protocol = RegExp.$4; 906 | var domain = RegExp.$5; 907 | var path = RegExp.$7; 908 | var endPosition = extractUrl.lastIndex; 909 | var startPosition = endPosition - url.length; 910 | 911 | if (!isValidUrl(url, protocol || DEFAULT_PROTOCOL, domain)) { 912 | return 'continue'; 913 | } 914 | // extract ASCII-only domains. 915 | if (!protocol) { 916 | if (!options.extractUrlsWithoutProtocol || before.match(invalidUrlWithoutProtocolPrecedingChars)) { 917 | return 'continue'; 918 | } 919 | 920 | var lastUrl = null; 921 | var asciiEndPosition = 0; 922 | domain.replace(validAsciiDomain, function (asciiDomain) { 923 | var asciiStartPosition = domain.indexOf(asciiDomain, asciiEndPosition); 924 | asciiEndPosition = asciiStartPosition + asciiDomain.length; 925 | lastUrl = { 926 | url: asciiDomain, 927 | indices: [startPosition + asciiStartPosition, startPosition + asciiEndPosition] 928 | }; 929 | if (path || asciiDomain.match(validSpecialShortDomain) || !asciiDomain.match(invalidShortDomain)) { 930 | urls.push(lastUrl); 931 | } 932 | }); 933 | 934 | // no ASCII-only domain found. Skip the entire URL. 935 | if (lastUrl == null) { 936 | return 'continue'; 937 | } 938 | 939 | // lastUrl only contains domain. Need to add path and query if they exist. 940 | if (path) { 941 | lastUrl.url = url.replace(domain, lastUrl.url); 942 | lastUrl.indices[1] = endPosition; 943 | } 944 | } else { 945 | // In the case of t.co URLs, don't allow additional path characters. 946 | if (url.match(validTcoUrl)) { 947 | var tcoUrlSlug = RegExp.$1; 948 | if (tcoUrlSlug && tcoUrlSlug.length > MAX_TCO_SLUG_LENGTH) { 949 | return 'continue'; 950 | } else { 951 | url = RegExp.lastMatch; 952 | endPosition = startPosition + url.length; 953 | } 954 | } 955 | urls.push({ 956 | url: url, 957 | indices: [startPosition, endPosition] 958 | }); 959 | } 960 | }; 961 | 962 | while (extractUrl.exec(text)) { 963 | var _ret = _loop(); 964 | 965 | if (_ret === 'continue') continue; 966 | } 967 | 968 | return urls; 969 | }; 970 | 971 | var isValidUrl = function isValidUrl(url, protocol, domain) { 972 | var urlLength = url.length; 973 | var punycodeEncodedDomain = idna.toAscii(domain); 974 | if (!punycodeEncodedDomain || !punycodeEncodedDomain.length) { 975 | return false; 976 | } 977 | 978 | urlLength = urlLength + punycodeEncodedDomain.length - domain.length; 979 | return protocol.length + urlLength <= MAX_URL_LENGTH; 980 | }; 981 | 982 | var removeOverlappingEntities = function (entities) { 983 | entities.sort(function (a, b) { 984 | return a.indices[0] - b.indices[0]; 985 | }); 986 | 987 | var prev = entities[0]; 988 | for (var i = 1; i < entities.length; i++) { 989 | if (prev.indices[1] > entities[i].indices[0]) { 990 | entities.splice(i, 1); 991 | i--; 992 | } else { 993 | prev = entities[i]; 994 | } 995 | } 996 | }; 997 | 998 | // Generated from unicode_regex/unicode_regex_groups.scala, same as objective c's \p{L}\p{M} 999 | var 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]/; 1000 | 1001 | // Generated from unicode_regex/unicode_regex_groups.scala, same as objective c's \p{L}\p{M} 1002 | var 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/; 1003 | 1004 | var nonBmpCodePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/mg; 1005 | 1006 | // A hashtag must contain at least one unicode letter or mark, as well as numbers, underscores, and select special characters. 1007 | var hashtagAlpha = regexSupplant(/(?:[#{bmpLetterAndMarks}]|(?=#{nonBmpCodePairs})(?:#{astralLetterAndMarks}))/, { bmpLetterAndMarks: bmpLetterAndMarks, nonBmpCodePairs: nonBmpCodePairs, astralLetterAndMarks: astralLetterAndMarks }); 1008 | 1009 | var 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]/; 1010 | 1011 | var 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/; 1012 | 1013 | var hashtagSpecialChars = /_\u200c\u200d\ua67e\u05be\u05f3\u05f4\uff5e\u301c\u309b\u309c\u30a0\u30fb\u3003\u0f0b\u0f0c\xb7/; 1014 | 1015 | var hashtagAlphaNumeric = regexSupplant(/(?:[#{bmpLetterAndMarks}#{bmpNumerals}#{hashtagSpecialChars}]|(?=#{nonBmpCodePairs})(?:#{astralLetterAndMarks}|#{astralNumerals}))/, { bmpLetterAndMarks: bmpLetterAndMarks, bmpNumerals: bmpNumerals, hashtagSpecialChars: hashtagSpecialChars, nonBmpCodePairs: nonBmpCodePairs, astralLetterAndMarks: astralLetterAndMarks, astralNumerals: astralNumerals }); 1016 | 1017 | var codePoint = /(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])/; 1018 | 1019 | var hashtagBoundary = regexSupplant(/(?:^|\uFE0E|\uFE0F|$|(?!#{hashtagAlphaNumeric}|&)#{codePoint})/, { codePoint: codePoint, hashtagAlphaNumeric: hashtagAlphaNumeric }); 1020 | 1021 | var validHashtag = regexSupplant(/(#{hashtagBoundary})(#{hashSigns})(?!\uFE0F|\u20E3)(#{hashtagAlphaNumeric}*#{hashtagAlpha}#{hashtagAlphaNumeric}*)/gi, { hashtagBoundary: hashtagBoundary, hashSigns: hashSigns, hashtagAlphaNumeric: hashtagAlphaNumeric, hashtagAlpha: hashtagAlpha }); 1022 | 1023 | var extractHashtagsWithIndices = function extractHashtagsWithIndices(text, options) { 1024 | if (!options) { 1025 | options = { checkUrlOverlap: true }; 1026 | } 1027 | 1028 | if (!text || !text.match(hashSigns)) { 1029 | return []; 1030 | } 1031 | 1032 | var tags = []; 1033 | 1034 | text.replace(validHashtag, function (match, before, hash, hashText, offset, chunk) { 1035 | var after = chunk.slice(offset + match.length); 1036 | if (after.match(endHashtagMatch)) { 1037 | return; 1038 | } 1039 | var startPosition = offset + before.length; 1040 | var endPosition = startPosition + hashText.length + 1; 1041 | tags.push({ 1042 | hashtag: hashText, 1043 | indices: [startPosition, endPosition] 1044 | }); 1045 | }); 1046 | 1047 | if (options.checkUrlOverlap) { 1048 | // also extract URL entities 1049 | var urls = extractUrlsWithIndices(text); 1050 | if (urls.length > 0) { 1051 | var entities = tags.concat(urls); 1052 | // remove overlap 1053 | removeOverlappingEntities(entities); 1054 | // only push back hashtags 1055 | tags = []; 1056 | for (var i = 0; i < entities.length; i++) { 1057 | if (entities[i].hashtag) { 1058 | tags.push(entities[i]); 1059 | } 1060 | } 1061 | } 1062 | } 1063 | 1064 | return tags; 1065 | }; 1066 | 1067 | var atSigns = /[@@]/; 1068 | 1069 | var endMentionMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/, { atSigns: atSigns, latinAccentChars: latinAccentChars }); 1070 | 1071 | var validMentionPrecedingChars = /(?:^|[^a-zA-Z0-9_!#$%&*@@]|(?:^|[^a-zA-Z0-9_+~.-])(?:rt|RT|rT|Rt):?)/; 1072 | 1073 | var validMentionOrList = regexSupplant('(#{validMentionPrecedingChars})' + // $1: Preceding character 1074 | '(#{atSigns})' + // $2: At mark 1075 | '([a-zA-Z0-9_]{1,20})' + // $3: Screen name 1076 | '(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?', // $4: List (optional) 1077 | { validMentionPrecedingChars: validMentionPrecedingChars, atSigns: atSigns }, 'g'); 1078 | 1079 | var extractMentionsOrListsWithIndices = function (text) { 1080 | if (!text || !text.match(atSigns)) { 1081 | return []; 1082 | } 1083 | 1084 | var possibleNames = []; 1085 | 1086 | text.replace(validMentionOrList, function (match, before, atSign, screenName, slashListname, offset, chunk) { 1087 | var after = chunk.slice(offset + match.length); 1088 | if (!after.match(endMentionMatch)) { 1089 | slashListname = slashListname || ''; 1090 | var startPosition = offset + before.length; 1091 | var endPosition = startPosition + screenName.length + slashListname.length + 1; 1092 | possibleNames.push({ 1093 | screenName: screenName, 1094 | listSlug: slashListname, 1095 | indices: [startPosition, endPosition] 1096 | }); 1097 | } 1098 | }); 1099 | 1100 | return possibleNames; 1101 | }; 1102 | 1103 | var extractEntitiesWithIndices = function (text, options) { 1104 | var entities = extractUrlsWithIndices(text, options).concat(extractMentionsOrListsWithIndices(text)).concat(extractHashtagsWithIndices(text, { checkUrlOverlap: false })).concat(extractCashtagsWithIndices(text)); 1105 | 1106 | if (entities.length == 0) { 1107 | return []; 1108 | } 1109 | 1110 | removeOverlappingEntities(entities); 1111 | return entities; 1112 | }; 1113 | 1114 | var clone = function (o) { 1115 | var r = {}; 1116 | for (var k in o) { 1117 | if (o.hasOwnProperty(k)) { 1118 | r[k] = o[k]; 1119 | } 1120 | } 1121 | 1122 | return r; 1123 | }; 1124 | 1125 | var BOOLEAN_ATTRIBUTES = { 1126 | 'disabled': true, 1127 | 'readonly': true, 1128 | 'multiple': true, 1129 | 'checked': true 1130 | }; 1131 | 1132 | // Options which should not be passed as HTML attributes 1133 | var OPTIONS_NOT_ATTRIBUTES = { 1134 | 'urlClass': true, 1135 | 'listClass': true, 1136 | 'usernameClass': true, 1137 | 'hashtagClass': true, 1138 | 'cashtagClass': true, 1139 | 'usernameUrlBase': true, 1140 | 'listUrlBase': true, 1141 | 'hashtagUrlBase': true, 1142 | 'cashtagUrlBase': true, 1143 | 'usernameUrlBlock': true, 1144 | 'listUrlBlock': true, 1145 | 'hashtagUrlBlock': true, 1146 | 'linkUrlBlock': true, 1147 | 'usernameIncludeSymbol': true, 1148 | 'suppressLists': true, 1149 | 'suppressNoFollow': true, 1150 | 'targetBlank': true, 1151 | 'suppressDataScreenName': true, 1152 | 'urlEntities': true, 1153 | 'symbolTag': true, 1154 | 'textWithSymbolTag': true, 1155 | 'urlTarget': true, 1156 | 'invisibleTagAttrs': true, 1157 | 'linkAttributeBlock': true, 1158 | 'linkTextBlock': true, 1159 | 'htmlEscapeNonEntities': true 1160 | }; 1161 | 1162 | var extractHtmlAttrsFromOptions = function (options) { 1163 | var htmlAttrs = {}; 1164 | for (var k in options) { 1165 | var v = options[k]; 1166 | if (OPTIONS_NOT_ATTRIBUTES[k]) { 1167 | continue; 1168 | } 1169 | if (BOOLEAN_ATTRIBUTES[k]) { 1170 | v = v ? k : null; 1171 | } 1172 | if (v == null) { 1173 | continue; 1174 | } 1175 | htmlAttrs[k] = v; 1176 | } 1177 | return htmlAttrs; 1178 | }; 1179 | 1180 | var HTML_ENTITIES = { 1181 | '&': '&', 1182 | '>': '>', 1183 | '<': '<', 1184 | '"': '"', 1185 | "'": ''' 1186 | }; 1187 | 1188 | var htmlEscape = function (text) { 1189 | return text && text.replace(/[&"'><]/g, function (character) { 1190 | return HTML_ENTITIES[character]; 1191 | }); 1192 | }; 1193 | 1194 | var BOOLEAN_ATTRIBUTES$1 = { 1195 | 'disabled': true, 1196 | 'readonly': true, 1197 | 'multiple': true, 1198 | 'checked': true 1199 | }; 1200 | 1201 | var tagAttrs = function (attributes) { 1202 | var htmlAttrs = ''; 1203 | for (var k in attributes) { 1204 | var v = attributes[k]; 1205 | if (BOOLEAN_ATTRIBUTES$1[k]) { 1206 | v = v ? k : null; 1207 | } 1208 | if (v == null) { 1209 | continue; 1210 | } 1211 | htmlAttrs += ' ' + htmlEscape(k) + '="' + htmlEscape(v.toString()) + '"'; 1212 | } 1213 | return htmlAttrs; 1214 | }; 1215 | 1216 | var linkToText = function (entity, text, attributes, options) { 1217 | if (!options.suppressNoFollow) { 1218 | attributes.rel = 'nofollow'; 1219 | } 1220 | // if linkAttributeBlock is specified, call it to modify the attributes 1221 | if (options.linkAttributeBlock) { 1222 | options.linkAttributeBlock(entity, attributes); 1223 | } 1224 | // if linkTextBlock is specified, call it to get a new/modified link text 1225 | if (options.linkTextBlock) { 1226 | text = options.linkTextBlock(entity, text); 1227 | } 1228 | var d = { 1229 | text: text, 1230 | attr: tagAttrs(attributes) 1231 | }; 1232 | return stringSupplant('#{text}', d); 1233 | }; 1234 | 1235 | var linkToTextWithSymbol = function (entity, symbol, text, attributes, options) { 1236 | var taggedSymbol = options.symbolTag ? '<' + options.symbolTag + '>' + symbol + '' : symbol; 1237 | text = htmlEscape(text); 1238 | var taggedText = options.textWithSymbolTag ? '<' + options.textWithSymbolTag + '>' + text + '' : text; 1239 | 1240 | if (options.usernameIncludeSymbol || !symbol.match(twttr.txt.regexen.atSigns)) { 1241 | return linkToText(entity, taggedSymbol + taggedText, attributes, options); 1242 | } else { 1243 | return taggedSymbol + linkToText(entity, taggedText, attributes, options); 1244 | } 1245 | }; 1246 | 1247 | var linkToCashtag = function (entity, text, options) { 1248 | var cashtag = htmlEscape(entity.cashtag); 1249 | var attrs = clone(options.htmlAttrs || {}); 1250 | attrs.href = options.cashtagUrlBase + cashtag; 1251 | attrs.title = '$' + cashtag; 1252 | attrs['class'] = options.cashtagClass; 1253 | if (options.targetBlank) { 1254 | attrs.target = '_blank'; 1255 | } 1256 | 1257 | return linkToTextWithSymbol(entity, '$', cashtag, attrs, options); 1258 | }; 1259 | 1260 | var rtlChars = /[\u0600-\u06FF]|[\u0750-\u077F]|[\u0590-\u05FF]|[\uFE70-\uFEFF]/mg; 1261 | 1262 | var linkToHashtag = function (entity, text, options) { 1263 | var hash = text.substring(entity.indices[0], entity.indices[0] + 1); 1264 | var hashtag = htmlEscape(entity.hashtag); 1265 | var attrs = clone(options.htmlAttrs || {}); 1266 | attrs.href = options.hashtagUrlBase + hashtag; 1267 | attrs.title = '#' + hashtag; 1268 | attrs['class'] = options.hashtagClass; 1269 | if (hashtag.charAt(0).match(rtlChars)) { 1270 | attrs['class'] += ' rtl'; 1271 | } 1272 | if (options.targetBlank) { 1273 | attrs.target = '_blank'; 1274 | } 1275 | 1276 | return linkToTextWithSymbol(entity, hash, hashtag, attrs, options); 1277 | }; 1278 | 1279 | var linkTextWithEntity = function (entity, options) { 1280 | var displayUrl = entity.display_url; 1281 | var expandedUrl = entity.expanded_url; 1282 | 1283 | // Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste 1284 | // should contain the full original URL (expanded_url), not the display URL. 1285 | // 1286 | // Method: Whenever possible, we actually emit HTML that contains expanded_url, and use 1287 | // font-size:0 to hide those parts that should not be displayed (because they are not part of display_url). 1288 | // Elements with font-size:0 get copied even though they are not visible. 1289 | // Note that display:none doesn't work here. Elements with display:none don't get copied. 1290 | // 1291 | // Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we 1292 | // wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on 1293 | // everything with the tco-ellipsis class. 1294 | // 1295 | // Exception: pic.twitter.com images, for which expandedUrl = "https://twitter.com/#!/username/status/1234/photo/1 1296 | // For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts. 1297 | // For a pic.twitter.com URL, the only elided part will be the "https://", so this is fine. 1298 | 1299 | var displayUrlSansEllipses = displayUrl.replace(/…/g, ''); // We have to disregard ellipses for matching 1300 | // Note: we currently only support eliding parts of the URL at the beginning or the end. 1301 | // Eventually we may want to elide parts of the URL in the *middle*. If so, this code will 1302 | // become more complicated. We will probably want to create a regexp out of display URL, 1303 | // replacing every ellipsis with a ".*". 1304 | if (expandedUrl.indexOf(displayUrlSansEllipses) != -1) { 1305 | var displayUrlIndex = expandedUrl.indexOf(displayUrlSansEllipses); 1306 | var v = { 1307 | displayUrlSansEllipses: displayUrlSansEllipses, 1308 | // Portion of expandedUrl that precedes the displayUrl substring 1309 | beforeDisplayUrl: expandedUrl.substr(0, displayUrlIndex), 1310 | // Portion of expandedUrl that comes after displayUrl 1311 | afterDisplayUrl: expandedUrl.substr(displayUrlIndex + displayUrlSansEllipses.length), 1312 | precedingEllipsis: displayUrl.match(/^…/) ? '…' : '', 1313 | followingEllipsis: displayUrl.match(/…$/) ? '…' : '' 1314 | }; 1315 | for (var k in v) { 1316 | if (v.hasOwnProperty(k)) { 1317 | v[k] = htmlEscape(v[k]); 1318 | } 1319 | } 1320 | // As an example: The user tweets "hi http://longdomainname.com/foo" 1321 | // This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo" 1322 | // This will get rendered as: 1323 | // 1324 | // … 1325 | // 1333 | // http://longdomai 1334 | // 1335 | // 1336 | // nname.com/foo 1337 | // 1338 | // 1339 | //   1340 | // … 1341 | // 1342 | v['invisible'] = options.invisibleTagAttrs; 1343 | return stringSupplant("#{precedingEllipsis} #{beforeDisplayUrl}#{displayUrlSansEllipses}#{afterDisplayUrl} #{followingEllipsis}", v); 1344 | } 1345 | return displayUrl; 1346 | }; 1347 | 1348 | var urlHasProtocol = /^https?:\/\//i; 1349 | 1350 | var linkToUrl = function (entity, text, options) { 1351 | var url = entity.url; 1352 | var displayUrl = url; 1353 | var linkText = htmlEscape(displayUrl); 1354 | 1355 | // If the caller passed a urlEntities object (provided by a Twitter API 1356 | // response with include_entities=true), we use that to render the display_url 1357 | // for each URL instead of it's underlying t.co URL. 1358 | var urlEntity = options.urlEntities && options.urlEntities[url] || entity; 1359 | if (urlEntity.display_url) { 1360 | linkText = linkTextWithEntity(urlEntity, options); 1361 | } 1362 | 1363 | var attrs = clone(options.htmlAttrs || {}); 1364 | 1365 | if (!url.match(urlHasProtocol)) { 1366 | url = 'http://' + url; 1367 | } 1368 | attrs.href = url; 1369 | 1370 | if (options.targetBlank) { 1371 | attrs.target = '_blank'; 1372 | } 1373 | 1374 | // set class only if urlClass is specified. 1375 | if (options.urlClass) { 1376 | attrs['class'] = options.urlClass; 1377 | } 1378 | 1379 | // set target only if urlTarget is specified. 1380 | if (options.urlTarget) { 1381 | attrs.target = options.urlTarget; 1382 | } 1383 | 1384 | if (!options.title && urlEntity.display_url) { 1385 | attrs.title = urlEntity.expanded_url; 1386 | } 1387 | 1388 | return linkToText(entity, linkText, attrs, options); 1389 | }; 1390 | 1391 | var linkToMentionAndList = function (entity, text, options) { 1392 | var at = text.substring(entity.indices[0], entity.indices[0] + 1); 1393 | var user = htmlEscape(entity.screenName); 1394 | var slashListname = htmlEscape(entity.listSlug); 1395 | var isList = entity.listSlug && !options.suppressLists; 1396 | var attrs = clone(options.htmlAttrs || {}); 1397 | attrs['class'] = isList ? options.listClass : options.usernameClass; 1398 | attrs.href = isList ? options.listUrlBase + user + slashListname : options.usernameUrlBase + user; 1399 | if (!isList && !options.suppressDataScreenName) { 1400 | attrs['data-screen-name'] = user; 1401 | } 1402 | if (options.targetBlank) { 1403 | attrs.target = '_blank'; 1404 | } 1405 | 1406 | return linkToTextWithSymbol(entity, at, isList ? user + slashListname : user, attrs, options); 1407 | }; 1408 | 1409 | // Default CSS class for auto-linked lists (along with the url class) 1410 | var DEFAULT_LIST_CLASS = 'tweet-url list-slug'; 1411 | // Default CSS class for auto-linked usernames (along with the url class) 1412 | var DEFAULT_USERNAME_CLASS = 'tweet-url username'; 1413 | // Default CSS class for auto-linked hashtags (along with the url class) 1414 | var DEFAULT_HASHTAG_CLASS = 'tweet-url hashtag'; 1415 | // Default CSS class for auto-linked cashtags (along with the url class) 1416 | var DEFAULT_CASHTAG_CLASS = 'tweet-url cashtag'; 1417 | 1418 | var autoLinkEntities = function (text, entities, options) { 1419 | var options = clone(options || {}); 1420 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 1421 | options.hashtagUrlBase = options.hashtagUrlBase || 'https://twitter.com/search?q=%23'; 1422 | options.cashtagClass = options.cashtagClass || DEFAULT_CASHTAG_CLASS; 1423 | options.cashtagUrlBase = options.cashtagUrlBase || 'https://twitter.com/search?q=%24'; 1424 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 1425 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 1426 | options.usernameUrlBase = options.usernameUrlBase || 'https://twitter.com/'; 1427 | options.listUrlBase = options.listUrlBase || 'https://twitter.com/'; 1428 | options.htmlAttrs = extractHtmlAttrsFromOptions(options); 1429 | options.invisibleTagAttrs = options.invisibleTagAttrs || "style='position:absolute;left:-9999px;'"; 1430 | 1431 | // remap url entities to hash 1432 | var urlEntities, i, len; 1433 | if (options.urlEntities) { 1434 | urlEntities = {}; 1435 | for (i = 0, len = options.urlEntities.length; i < len; i++) { 1436 | urlEntities[options.urlEntities[i].url] = options.urlEntities[i]; 1437 | } 1438 | options.urlEntities = urlEntities; 1439 | } 1440 | 1441 | var result = ''; 1442 | var beginIndex = 0; 1443 | 1444 | // sort entities by start index 1445 | entities.sort(function (a, b) { 1446 | return a.indices[0] - b.indices[0]; 1447 | }); 1448 | 1449 | var nonEntity = options.htmlEscapeNonEntities ? htmlEscape : function (text) { 1450 | return text; 1451 | }; 1452 | 1453 | for (var i = 0; i < entities.length; i++) { 1454 | var entity = entities[i]; 1455 | result += nonEntity(text.substring(beginIndex, entity.indices[0])); 1456 | 1457 | if (entity.url) { 1458 | result += linkToUrl(entity, text, options); 1459 | } else if (entity.hashtag) { 1460 | result += linkToHashtag(entity, text, options); 1461 | } else if (entity.screenName) { 1462 | result += linkToMentionAndList(entity, text, options); 1463 | } else if (entity.cashtag) { 1464 | result += linkToCashtag(entity, text, options); 1465 | } 1466 | beginIndex = entity.indices[1]; 1467 | } 1468 | result += nonEntity(text.substring(beginIndex, text.length)); 1469 | return result; 1470 | }; 1471 | 1472 | var autoLink = function (text, options) { 1473 | var entities = extractEntitiesWithIndices(text, { extractUrlsWithoutProtocol: false }); 1474 | return autoLinkEntities(text, entities, options); 1475 | }; 1476 | 1477 | var autoLinkCashtags = function (text, options) { 1478 | var entities = extractCashtagsWithIndices(text); 1479 | return autoLinkEntities(text, entities, options); 1480 | }; 1481 | 1482 | var autoLinkHashtags = function (text, options) { 1483 | var entities = extractHashtagsWithIndices(text); 1484 | return autoLinkEntities(text, entities, options); 1485 | }; 1486 | 1487 | var autoLinkUrlsCustom = function (text, options) { 1488 | var entities = extractUrlsWithIndices(text, { extractUrlsWithoutProtocol: false }); 1489 | return autoLinkEntities(text, entities, options); 1490 | }; 1491 | 1492 | var autoLinkUsernamesOrLists = function (text, options) { 1493 | var entities = extractMentionsOrListsWithIndices(text); 1494 | return autoLinkEntities(text, entities, options); 1495 | }; 1496 | 1497 | /** 1498 | * Copied from https://github.com/twitter/twitter-text/blob/master/js/twitter-text.js 1499 | */ 1500 | 1501 | var convertUnicodeIndices = function convertUnicodeIndices(text, entities, indicesInUTF16) { 1502 | if (entities.length === 0) { 1503 | return; 1504 | } 1505 | 1506 | var charIndex = 0; 1507 | var codePointIndex = 0; 1508 | 1509 | // sort entities by start index 1510 | entities.sort(function (a, b) { 1511 | return a.indices[0] - b.indices[0]; 1512 | }); 1513 | var entityIndex = 0; 1514 | var entity = entities[0]; 1515 | 1516 | while (charIndex < text.length) { 1517 | if (entity.indices[0] === (indicesInUTF16 ? charIndex : codePointIndex)) { 1518 | var len = entity.indices[1] - entity.indices[0]; 1519 | entity.indices[0] = indicesInUTF16 ? codePointIndex : charIndex; 1520 | entity.indices[1] = entity.indices[0] + len; 1521 | 1522 | entityIndex++; 1523 | if (entityIndex === entities.length) { 1524 | // no more entity 1525 | break; 1526 | } 1527 | entity = entities[entityIndex]; 1528 | } 1529 | 1530 | var c = text.charCodeAt(charIndex); 1531 | if (c >= 0xD800 && c <= 0xDBFF && charIndex < text.length - 1) { 1532 | // Found high surrogate char 1533 | c = text.charCodeAt(charIndex + 1); 1534 | if (c >= 0xDC00 && c <= 0xDFFF) { 1535 | // Found surrogate pair 1536 | charIndex++; 1537 | } 1538 | } 1539 | codePointIndex++; 1540 | charIndex++; 1541 | } 1542 | }; 1543 | 1544 | var modifyIndicesFromUnicodeToUTF16 = function (text, entities) { 1545 | convertUnicodeIndices(text, entities, false); 1546 | }; 1547 | 1548 | var autoLinkWithJSON = function (text, json, options) { 1549 | // map JSON entity to twitter-text entity 1550 | if (json.user_mentions) { 1551 | for (var i = 0; i < json.user_mentions.length; i++) { 1552 | // this is a @mention 1553 | json.user_mentions[i].screenName = json.user_mentions[i].screen_name; 1554 | } 1555 | } 1556 | 1557 | if (json.hashtags) { 1558 | for (var i = 0; i < json.hashtags.length; i++) { 1559 | // this is a #hashtag 1560 | json.hashtags[i].hashtag = json.hashtags[i].text; 1561 | } 1562 | } 1563 | 1564 | if (json.symbols) { 1565 | for (var i = 0; i < json.symbols.length; i++) { 1566 | // this is a $CASH tag 1567 | json.symbols[i].cashtag = json.symbols[i].text; 1568 | } 1569 | } 1570 | 1571 | // concatenate all entities 1572 | var entities = []; 1573 | for (var key in json) { 1574 | entities = entities.concat(json[key]); 1575 | } 1576 | 1577 | // modify indices to UTF-16 1578 | modifyIndicesFromUnicodeToUTF16(text, entities); 1579 | 1580 | return autoLinkEntities(text, entities, options); 1581 | }; 1582 | 1583 | var version = 1; 1584 | var maxWeightedTweetLength = 140; 1585 | var scale = 1; 1586 | var defaultWeight = 1; 1587 | var transformedURLLength = 23; 1588 | var ranges = []; 1589 | var version1 = { 1590 | version: version, 1591 | maxWeightedTweetLength: maxWeightedTweetLength, 1592 | scale: scale, 1593 | defaultWeight: defaultWeight, 1594 | transformedURLLength: transformedURLLength, 1595 | ranges: ranges 1596 | }; 1597 | 1598 | var version$1 = 2; 1599 | var maxWeightedTweetLength$1 = 280; 1600 | var scale$1 = 100; 1601 | var defaultWeight$1 = 200; 1602 | var transformedURLLength$1 = 23; 1603 | var ranges$1 = [{"start":0,"end":4351,"weight":100},{"start":8192,"end":8205,"weight":100},{"start":8208,"end":8223,"weight":100},{"start":8242,"end":8247,"weight":100}]; 1604 | var version2 = { 1605 | version: version$1, 1606 | maxWeightedTweetLength: maxWeightedTweetLength$1, 1607 | scale: scale$1, 1608 | defaultWeight: defaultWeight$1, 1609 | transformedURLLength: transformedURLLength$1, 1610 | ranges: ranges$1 1611 | }; 1612 | 1613 | // These json files are created by the build script 1614 | var defaults$1 = version2; 1615 | 1616 | var configs = { 1617 | defaults: defaults$1, 1618 | version1: version1, 1619 | version2: version2 1620 | }; 1621 | 1622 | var convertUnicodeIndices$2 = function (text, entities, indicesInUTF16) { 1623 | if (entities.length == 0) { 1624 | return; 1625 | } 1626 | 1627 | var charIndex = 0; 1628 | var codePointIndex = 0; 1629 | 1630 | // sort entities by start index 1631 | entities.sort(function (a, b) { 1632 | return a.indices[0] - b.indices[0]; 1633 | }); 1634 | var entityIndex = 0; 1635 | var entity = entities[0]; 1636 | 1637 | while (charIndex < text.length) { 1638 | if (entity.indices[0] == (indicesInUTF16 ? charIndex : codePointIndex)) { 1639 | var len = entity.indices[1] - entity.indices[0]; 1640 | entity.indices[0] = indicesInUTF16 ? codePointIndex : charIndex; 1641 | entity.indices[1] = entity.indices[0] + len; 1642 | 1643 | entityIndex++; 1644 | if (entityIndex == entities.length) { 1645 | // no more entity 1646 | break; 1647 | } 1648 | entity = entities[entityIndex]; 1649 | } 1650 | 1651 | var c = text.charCodeAt(charIndex); 1652 | if (c >= 0xD800 && c <= 0xDBFF && charIndex < text.length - 1) { 1653 | // Found high surrogate char 1654 | c = text.charCodeAt(charIndex + 1); 1655 | if (c >= 0xDC00 && c <= 0xDFFF) { 1656 | // Found surrogate pair 1657 | charIndex++; 1658 | } 1659 | } 1660 | codePointIndex++; 1661 | charIndex++; 1662 | } 1663 | }; 1664 | 1665 | var extractCashtags = function (text) { 1666 | var cashtagsOnly = [], 1667 | cashtagsWithIndices = extractCashtagsWithIndices(text); 1668 | 1669 | for (var i = 0; i < cashtagsWithIndices.length; i++) { 1670 | cashtagsOnly.push(cashtagsWithIndices[i].cashtag); 1671 | } 1672 | 1673 | return cashtagsOnly; 1674 | }; 1675 | 1676 | var extractHashtags = function (text) { 1677 | var hashtagsOnly = []; 1678 | var hashtagsWithIndices = extractHashtagsWithIndices(text); 1679 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 1680 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 1681 | } 1682 | 1683 | return hashtagsOnly; 1684 | }; 1685 | 1686 | var extractMentionsWithIndices = function (text) { 1687 | var mentions = []; 1688 | var mentionOrList = void 0; 1689 | var mentionsOrLists = extractMentionsOrListsWithIndices(text); 1690 | 1691 | for (var i = 0; i < mentionsOrLists.length; i++) { 1692 | mentionOrList = mentionsOrLists[i]; 1693 | if (mentionOrList.listSlug === '') { 1694 | mentions.push({ 1695 | screenName: mentionOrList.screenName, 1696 | indices: mentionOrList.indices 1697 | }); 1698 | } 1699 | } 1700 | 1701 | return mentions; 1702 | }; 1703 | 1704 | var extractMentions = function (text) { 1705 | var screenNamesOnly = [], 1706 | screenNamesWithIndices = extractMentionsWithIndices(text); 1707 | 1708 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 1709 | var screenName = screenNamesWithIndices[i].screenName; 1710 | screenNamesOnly.push(screenName); 1711 | } 1712 | 1713 | return screenNamesOnly; 1714 | }; 1715 | 1716 | var validReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/, { atSigns: atSigns, spaces: spaces }); 1717 | 1718 | var extractReplies = function (text) { 1719 | if (!text) { 1720 | return null; 1721 | } 1722 | 1723 | var possibleScreenName = text.match(validReply); 1724 | if (!possibleScreenName || RegExp.rightContext.match(endMentionMatch)) { 1725 | return null; 1726 | } 1727 | 1728 | return possibleScreenName[1]; 1729 | }; 1730 | 1731 | var extractUrls = function (text, options) { 1732 | var urlsOnly = []; 1733 | var urlsWithIndices = extractUrlsWithIndices(text, options); 1734 | 1735 | for (var i = 0; i < urlsWithIndices.length; i++) { 1736 | urlsOnly.push(urlsWithIndices[i].url); 1737 | } 1738 | 1739 | return urlsOnly; 1740 | }; 1741 | 1742 | var getCharacterWeight = function getCharacterWeight(ch, options) { 1743 | var defaultWeight = options.defaultWeight, 1744 | ranges = options.ranges; 1745 | 1746 | var weight = defaultWeight; 1747 | var chCodePoint = ch.charCodeAt(0); 1748 | if (Array.isArray(ranges)) { 1749 | for (var i = 0, length = ranges.length; i < length; i++) { 1750 | var currRange = ranges[i]; 1751 | if (chCodePoint >= currRange.start && chCodePoint <= currRange.end) { 1752 | weight = currRange.weight; 1753 | break; 1754 | } 1755 | } 1756 | } 1757 | 1758 | return weight; 1759 | }; 1760 | 1761 | var modifyIndicesFromUTF16ToUnicode = function (text, entities) { 1762 | convertUnicodeIndices(text, entities, true); 1763 | }; 1764 | 1765 | var invalidChars = regexSupplant(/[#{invalidCharsGroup}]/, { invalidCharsGroup: invalidCharsGroup }); 1766 | 1767 | var hasInvalidCharacters = function (text) { 1768 | return invalidChars.test(text); 1769 | }; 1770 | 1771 | var urlHasHttps = /^https:\/\//i; 1772 | 1773 | /** 1774 | * [parseTweet description] 1775 | * @param {string} text tweet text to parse 1776 | * @param {Object} options config options to pass 1777 | * @return {Object} Fields in response described below: 1778 | * 1779 | * Response fields: 1780 | * weightedLength {int} the weighted length of tweet based on weights specified in the config 1781 | * valid {bool} If tweet is valid 1782 | * permillage {float} permillage of the tweet over the max length specified in config 1783 | * validRangeStart {int} beginning of valid text 1784 | * validRangeEnd {int} End index of valid part of the tweet text (inclusive) in utf16 1785 | * displayRangeStart {int} beginning index of display text 1786 | * displayRangeEnd {int} end index of display text (inclusive) in utf16 1787 | */ 1788 | var parseTweet = function parseTweet() { 1789 | var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; 1790 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : configs.defaults; 1791 | 1792 | var mergedOptions = _extends({}, configs.defaults, options); 1793 | var defaultWeight = mergedOptions.defaultWeight, 1794 | scale = mergedOptions.scale, 1795 | maxWeightedTweetLength = mergedOptions.maxWeightedTweetLength, 1796 | transformedURLLength = mergedOptions.transformedURLLength; 1797 | 1798 | var normalizedText = typeof String.prototype.normalize === 'function' ? text.normalize() : text; 1799 | var urlsWithIndices = extractUrlsWithIndices(normalizedText); 1800 | var tweetLength = normalizedText.length; 1801 | 1802 | var weightedLength = 0; 1803 | var validDisplayIndex = 0; 1804 | var valid = true; 1805 | // Go through every character and calculate weight 1806 | 1807 | var _loop = function _loop(_charIndex) { 1808 | // If a url begins at the specified index handle, add constant length 1809 | var urlEntity = urlsWithIndices.filter(function (_ref) { 1810 | var indices = _ref.indices; 1811 | return indices[0] === _charIndex; 1812 | })[0]; 1813 | if (urlEntity) { 1814 | var url = urlEntity.url; 1815 | 1816 | weightedLength += transformedURLLength * scale; 1817 | _charIndex += url.length - 1; 1818 | } else { 1819 | if (isSurrogatePair(normalizedText, _charIndex)) { 1820 | _charIndex += 1; 1821 | } 1822 | weightedLength += getCharacterWeight(normalizedText.charAt(_charIndex), mergedOptions); 1823 | } 1824 | 1825 | // Only test for validity of character if it is still valid 1826 | if (valid) { 1827 | valid = !hasInvalidCharacters(normalizedText.substring(_charIndex, _charIndex + 1)); 1828 | } 1829 | if (valid && weightedLength <= maxWeightedTweetLength * scale) { 1830 | validDisplayIndex = _charIndex; 1831 | } 1832 | charIndex = _charIndex; 1833 | }; 1834 | 1835 | for (var charIndex = 0; charIndex < tweetLength; charIndex++) { 1836 | _loop(charIndex); 1837 | } 1838 | 1839 | weightedLength = weightedLength / scale; 1840 | valid = valid && weightedLength > 0 && weightedLength <= maxWeightedTweetLength; 1841 | var permillage = Math.floor(weightedLength / maxWeightedTweetLength * 1000); 1842 | var normalizationOffset = text.length - normalizedText.length; 1843 | validDisplayIndex += normalizationOffset; 1844 | 1845 | return { 1846 | weightedLength: weightedLength, 1847 | valid: valid, 1848 | permillage: permillage, 1849 | validRangeStart: 0, 1850 | validRangeEnd: validDisplayIndex, 1851 | displayRangeStart: 0, 1852 | displayRangeEnd: text.length > 0 ? text.length - 1 : 0 1853 | }; 1854 | }; 1855 | 1856 | var isSurrogatePair = function isSurrogatePair(text, cIndex) { 1857 | // Test if a character is the beginning of a surrogate pair 1858 | if (cIndex < text.length - 1) { 1859 | var c = text.charCodeAt(cIndex); 1860 | var cNext = text.charCodeAt(cIndex + 1); 1861 | return 0xD800 <= c && c <= 0xDBFF && 0xDC00 <= cNext && cNext <= 0xDFFF; 1862 | } 1863 | return false; 1864 | }; 1865 | 1866 | var getTweetLength = function getTweetLength(text) { 1867 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : configs.defaults; 1868 | 1869 | return parseTweet(text, options).weightedLength; 1870 | }; 1871 | 1872 | /** 1873 | * Copied from https://github.com/twitter/twitter-text/blob/master/js/twitter-text.js 1874 | */ 1875 | var getUnicodeTextLength = function (text) { 1876 | return text.replace(nonBmpCodePairs, ' ').length; 1877 | }; 1878 | 1879 | // this essentially does text.split(/<|>/) 1880 | // except that won't work in IE, where empty strings are ommitted 1881 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 1882 | // but "<<".split("<") => ["", "", ""] 1883 | var splitTags = function (text) { 1884 | var firstSplits = text.split('<'), 1885 | secondSplits = void 0, 1886 | allSplits = [], 1887 | split = void 0; 1888 | 1889 | for (var i = 0; i < firstSplits.length; i += 1) { 1890 | split = firstSplits[i]; 1891 | if (!split) { 1892 | allSplits.push(''); 1893 | } else { 1894 | secondSplits = split.split('>'); 1895 | for (var j = 0; j < secondSplits.length; j += 1) { 1896 | allSplits.push(secondSplits[j]); 1897 | } 1898 | } 1899 | } 1900 | 1901 | return allSplits; 1902 | }; 1903 | 1904 | var hitHighlight = function (text, hits, options) { 1905 | var defaultHighlightTag = 'em'; 1906 | 1907 | hits = hits || []; 1908 | options = options || {}; 1909 | 1910 | if (hits.length === 0) { 1911 | return text; 1912 | } 1913 | 1914 | var tagName = options.tag || defaultHighlightTag, 1915 | tags = ['<' + tagName + '>', ''], 1916 | chunks = splitTags(text), 1917 | i = void 0, 1918 | j = void 0, 1919 | result = '', 1920 | chunkIndex = 0, 1921 | chunk = chunks[0], 1922 | prevChunksLen = 0, 1923 | chunkCursor = 0, 1924 | startInChunk = false, 1925 | chunkChars = chunk, 1926 | flatHits = [], 1927 | index = void 0, 1928 | hit = void 0, 1929 | tag = void 0, 1930 | placed = void 0, 1931 | hitSpot = void 0; 1932 | 1933 | for (i = 0; i < hits.length; i += 1) { 1934 | for (j = 0; j < hits[i].length; j += 1) { 1935 | flatHits.push(hits[i][j]); 1936 | } 1937 | } 1938 | 1939 | for (index = 0; index < flatHits.length; index += 1) { 1940 | hit = flatHits[index]; 1941 | tag = tags[index % 2]; 1942 | placed = false; 1943 | 1944 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 1945 | result += chunkChars.slice(chunkCursor); 1946 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 1947 | result += tag; 1948 | placed = true; 1949 | } 1950 | 1951 | if (chunks[chunkIndex + 1]) { 1952 | result += '<' + chunks[chunkIndex + 1] + '>'; 1953 | } 1954 | 1955 | prevChunksLen += chunkChars.length; 1956 | chunkCursor = 0; 1957 | chunkIndex += 2; 1958 | chunk = chunks[chunkIndex]; 1959 | chunkChars = chunk; 1960 | startInChunk = false; 1961 | } 1962 | 1963 | if (!placed && chunk != null) { 1964 | hitSpot = hit - prevChunksLen; 1965 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 1966 | chunkCursor = hitSpot; 1967 | if (index % 2 === 0) { 1968 | startInChunk = true; 1969 | } else { 1970 | startInChunk = false; 1971 | } 1972 | } else if (!placed) { 1973 | placed = true; 1974 | result += tag; 1975 | } 1976 | } 1977 | 1978 | if (chunk != null) { 1979 | if (chunkCursor < chunkChars.length) { 1980 | result += chunkChars.slice(chunkCursor); 1981 | } 1982 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 1983 | result += index % 2 === 0 ? chunks[index] : '<' + chunks[index] + '>'; 1984 | } 1985 | } 1986 | 1987 | return result; 1988 | }; 1989 | 1990 | var isInvalidTweet = function (text) { 1991 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : configs.defaults; 1992 | 1993 | if (!text) { 1994 | return 'empty'; 1995 | } 1996 | 1997 | var mergedOptions = _extends({}, configs.defaults, options); 1998 | var maxLength = mergedOptions.maxWeightedTweetLength; 1999 | 2000 | // Determine max length independent of URL length 2001 | if (getTweetLength(text, mergedOptions) > maxLength) { 2002 | return 'too_long'; 2003 | } 2004 | 2005 | if (hasInvalidCharacters(text)) { 2006 | return 'invalid_characters'; 2007 | } 2008 | 2009 | return false; 2010 | }; 2011 | 2012 | var isValidHashtag = function (hashtag) { 2013 | if (!hashtag) { 2014 | return false; 2015 | } 2016 | 2017 | var extracted = extractHashtags(hashtag); 2018 | 2019 | // Should extract the hashtag minus the # sign, hence the .slice(1) 2020 | return extracted.length === 1 && extracted[0] === hashtag.slice(1); 2021 | }; 2022 | 2023 | var VALID_LIST_RE = regexSupplant(/^#{validMentionOrList}$/, { validMentionOrList: validMentionOrList }); 2024 | 2025 | var isValidList = function (usernameList) { 2026 | var match = usernameList.match(VALID_LIST_RE); 2027 | 2028 | // Must have matched and had nothing before or after 2029 | return !!(match && match[1] == '' && match[4]); 2030 | }; 2031 | 2032 | var isValidTweetText = function (text, options) { 2033 | return !isInvalidTweet(text, options); 2034 | }; 2035 | 2036 | var validateUrlUnreserved = /[a-z\u0400-\u04FF0-9\-._~]/i; 2037 | 2038 | var validateUrlPctEncoded = /(?:%[0-9a-f]{2})/i; 2039 | 2040 | var validateUrlSubDelims = /[!$&'()*+,;=]/i; 2041 | 2042 | var validateUrlUserinfo = regexSupplant('(?:' + '#{validateUrlUnreserved}|' + '#{validateUrlPctEncoded}|' + '#{validateUrlSubDelims}|' + ':' + ')*', { validateUrlUnreserved: validateUrlUnreserved, validateUrlPctEncoded: validateUrlPctEncoded, validateUrlSubDelims: validateUrlSubDelims }, 'i'); 2043 | 2044 | var validateUrlDomainSegment = /(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?)/i; 2045 | 2046 | var validateUrlDomainTld = /(?:[a-z](?:[a-z0-9\-]*[a-z0-9])?)/i; 2047 | 2048 | var validateUrlSubDomainSegment = /(?:[a-z0-9](?:[a-z0-9_\-]*[a-z0-9])?)/i; 2049 | 2050 | var validateUrlDomain = regexSupplant(/(?:(?:#{validateUrlSubDomainSegment}\.)*(?:#{validateUrlDomainSegment}\.)#{validateUrlDomainTld})/i, { validateUrlSubDomainSegment: validateUrlSubDomainSegment, validateUrlDomainSegment: validateUrlDomainSegment, validateUrlDomainTld: validateUrlDomainTld }); 2051 | 2052 | var validateUrlDecOctet = /(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9]{2})|(?:2[0-4][0-9])|(?:25[0-5]))/i; 2053 | 2054 | var validateUrlIpv4 = regexSupplant(/(?:#{validateUrlDecOctet}(?:\.#{validateUrlDecOctet}){3})/i, { validateUrlDecOctet: validateUrlDecOctet }); 2055 | 2056 | // Punting on real IPv6 validation for now 2057 | var validateUrlIpv6 = /(?:\[[a-f0-9:\.]+\])/i; 2058 | 2059 | // Punting on IPvFuture for now 2060 | var validateUrlIp = regexSupplant('(?:' + '#{validateUrlIpv4}|' + '#{validateUrlIpv6}' + ')', { validateUrlIpv4: validateUrlIpv4, validateUrlIpv6: validateUrlIpv6 }, 'i'); 2061 | 2062 | var validateUrlHost = regexSupplant('(?:' + '#{validateUrlIp}|' + '#{validateUrlDomain}' + ')', { validateUrlIp: validateUrlIp, validateUrlDomain: validateUrlDomain }, 'i'); 2063 | 2064 | var validateUrlPort = /[0-9]{1,5}/; 2065 | 2066 | var validateUrlAuthority = regexSupplant( 2067 | // $1 userinfo 2068 | '(?:(#{validateUrlUserinfo})@)?' + 2069 | // $2 host 2070 | '(#{validateUrlHost})' + 2071 | // $3 port 2072 | '(?::(#{validateUrlPort}))?', { validateUrlUserinfo: validateUrlUserinfo, validateUrlHost: validateUrlHost, validateUrlPort: validateUrlPort }, 'i'); 2073 | 2074 | // These URL validation pattern strings are based on the ABNF from RFC 3986 2075 | var validateUrlPchar = regexSupplant('(?:' + '#{validateUrlUnreserved}|' + '#{validateUrlPctEncoded}|' + '#{validateUrlSubDelims}|' + '[:|@]' + ')', { validateUrlUnreserved: validateUrlUnreserved, validateUrlPctEncoded: validateUrlPctEncoded, validateUrlSubDelims: validateUrlSubDelims }, 'i'); 2076 | 2077 | var validateUrlFragment = regexSupplant(/(#{validateUrlPchar}|\/|\?)*/i, { validateUrlPchar: validateUrlPchar }); 2078 | 2079 | var validateUrlPath = regexSupplant(/(\/#{validateUrlPchar}*)*/i, { validateUrlPchar: validateUrlPchar }); 2080 | 2081 | var validateUrlQuery = regexSupplant(/(#{validateUrlPchar}|\/|\?)*/i, { validateUrlPchar: validateUrlPchar }); 2082 | 2083 | var validateUrlScheme = /(?:[a-z][a-z0-9+\-.]*)/i; 2084 | 2085 | // Modified version of RFC 3986 Appendix B 2086 | var validateUrlUnencoded = regexSupplant('^' + // Full URL 2087 | '(?:' + '([^:/?#]+):\\/\\/' + // $1 Scheme 2088 | ')?' + '([^/?#]*)' + // $2 Authority 2089 | '([^?#]*)' + // $3 Path 2090 | '(?:' + '\\?([^#]*)' + // $4 Query 2091 | ')?' + '(?:' + '#(.*)' + // $5 Fragment 2092 | ')?$', 'i'); 2093 | 2094 | var validateUrlUnicodeSubDomainSegment = /(?:(?:[a-z0-9]|[^\u0000-\u007f])(?:(?:[a-z0-9_\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i; 2095 | 2096 | var validateUrlUnicodeDomainSegment = /(?:(?:[a-z0-9]|[^\u0000-\u007f])(?:(?:[a-z0-9\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i; 2097 | 2098 | // Unencoded internationalized domains - this doesn't check for invalid UTF-8 sequences 2099 | var validateUrlUnicodeDomainTld = /(?:(?:[a-z]|[^\u0000-\u007f])(?:(?:[a-z0-9\-]|[^\u0000-\u007f])*(?:[a-z0-9]|[^\u0000-\u007f]))?)/i; 2100 | 2101 | // Unencoded internationalized domains - this doesn't check for invalid UTF-8 sequences 2102 | var validateUrlUnicodeDomain = regexSupplant(/(?:(?:#{validateUrlUnicodeSubDomainSegment}\.)*(?:#{validateUrlUnicodeDomainSegment}\.)#{validateUrlUnicodeDomainTld})/i, { validateUrlUnicodeSubDomainSegment: validateUrlUnicodeSubDomainSegment, validateUrlUnicodeDomainSegment: validateUrlUnicodeDomainSegment, validateUrlUnicodeDomainTld: validateUrlUnicodeDomainTld }); 2103 | 2104 | var validateUrlUnicodeHost = regexSupplant('(?:' + '#{validateUrlIp}|' + '#{validateUrlUnicodeDomain}' + ')', { validateUrlIp: validateUrlIp, validateUrlUnicodeDomain: validateUrlUnicodeDomain }, 'i'); 2105 | 2106 | var validateUrlUnicodeAuthority = regexSupplant( 2107 | // $1 userinfo 2108 | '(?:(#{validateUrlUserinfo})@)?' + 2109 | // $2 host 2110 | '(#{validateUrlUnicodeHost})' + 2111 | // $3 port 2112 | '(?::(#{validateUrlPort}))?', { validateUrlUserinfo: validateUrlUserinfo, validateUrlUnicodeHost: validateUrlUnicodeHost, validateUrlPort: validateUrlPort }, 'i'); 2113 | 2114 | function isValidMatch(string, regex, optional) { 2115 | if (!optional) { 2116 | // RegExp["$&"] is the text of the last match 2117 | // blank strings are ok, but are falsy, so we check stringiness instead of truthiness 2118 | return typeof string === 'string' && string.match(regex) && RegExp['$&'] === string; 2119 | } 2120 | 2121 | // RegExp["$&"] is the text of the last match 2122 | return !string || string.match(regex) && RegExp['$&'] === string; 2123 | } 2124 | 2125 | var isValidUrl$1 = function (url, unicodeDomains, requireProtocol) { 2126 | if (unicodeDomains == null) { 2127 | unicodeDomains = true; 2128 | } 2129 | 2130 | if (requireProtocol == null) { 2131 | requireProtocol = true; 2132 | } 2133 | 2134 | if (!url) { 2135 | return false; 2136 | } 2137 | 2138 | var urlParts = url.match(validateUrlUnencoded); 2139 | 2140 | if (!urlParts || urlParts[0] !== url) { 2141 | return false; 2142 | } 2143 | 2144 | var scheme = urlParts[1], 2145 | authority = urlParts[2], 2146 | path = urlParts[3], 2147 | query = urlParts[4], 2148 | fragment = urlParts[5]; 2149 | 2150 | if (!((!requireProtocol || isValidMatch(scheme, validateUrlScheme) && scheme.match(/^https?$/i)) && isValidMatch(path, validateUrlPath) && isValidMatch(query, validateUrlQuery, true) && isValidMatch(fragment, validateUrlFragment, true))) { 2151 | return false; 2152 | } 2153 | 2154 | return unicodeDomains && isValidMatch(authority, validateUrlUnicodeAuthority) || !unicodeDomains && isValidMatch(authority, validateUrlAuthority); 2155 | }; 2156 | 2157 | var isValidUsername = function (username) { 2158 | if (!username) { 2159 | return false; 2160 | } 2161 | 2162 | var extracted = extractMentions(username); 2163 | 2164 | // Should extract the username minus the @ sign, hence the .slice(1) 2165 | return extracted.length === 1 && extracted[0] === username.slice(1); 2166 | }; 2167 | 2168 | (function () { 2169 | if (typeof Object.assign != 'function') { 2170 | // Must be writable: true, enumerable: false, configurable: true 2171 | Object.defineProperty(Object, "assign", { 2172 | value: function assign(target, varArgs) { 2173 | // .length of function is 2 2174 | 'use strict'; 2175 | 2176 | if (target == null) { 2177 | // TypeError if undefined or null 2178 | throw new TypeError('Cannot convert undefined or null to object'); 2179 | } 2180 | 2181 | var to = Object(target); 2182 | 2183 | for (var index = 1; index < arguments.length; index++) { 2184 | var nextSource = arguments[index]; 2185 | 2186 | if (nextSource != null) { 2187 | // Skip over if undefined or null 2188 | for (var nextKey in nextSource) { 2189 | // Avoid bugs when hasOwnProperty is shadowed 2190 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 2191 | to[nextKey] = nextSource[nextKey]; 2192 | } 2193 | } 2194 | } 2195 | } 2196 | return to; 2197 | }, 2198 | writable: true, 2199 | configurable: true 2200 | }); 2201 | } 2202 | })(); 2203 | 2204 | var regexen = { 2205 | astralLetterAndMarks: astralLetterAndMarks, 2206 | astralNumerals: astralNumerals, 2207 | atSigns: atSigns, 2208 | bmpLetterAndMarks: bmpLetterAndMarks, 2209 | bmpNumerals: bmpNumerals, 2210 | cashtag: cashtag, 2211 | codePoint: codePoint, 2212 | cyrillicLettersAndMarks: cyrillicLettersAndMarks, 2213 | endHashtagMatch: endHashtagMatch, 2214 | endMentionMatch: endMentionMatch, 2215 | // extractUrl, 2216 | hashSigns: hashSigns, 2217 | hashtagAlpha: hashtagAlpha, 2218 | hashtagAlphaNumeric: hashtagAlphaNumeric, 2219 | hashtagBoundary: hashtagBoundary, 2220 | hashtagSpecialChars: hashtagSpecialChars, 2221 | invalidChars: invalidChars, 2222 | invalidCharsGroup: invalidCharsGroup, 2223 | invalidDomainChars: invalidDomainChars, 2224 | invalidShortDomain: invalidShortDomain, 2225 | invalidUrlWithoutProtocolPrecedingChars: invalidUrlWithoutProtocolPrecedingChars, 2226 | latinAccentChars: latinAccentChars, 2227 | nonBmpCodePairs: nonBmpCodePairs, 2228 | punct: punct, 2229 | rtlChars: rtlChars, 2230 | spaces: spaces, 2231 | spacesGroup: spacesGroup, 2232 | urlHasHttps: urlHasHttps, 2233 | urlHasProtocol: urlHasProtocol, 2234 | validAsciiDomain: validAsciiDomain, 2235 | validateUrlAuthority: validateUrlAuthority, 2236 | validateUrlDecOctet: validateUrlDecOctet, 2237 | validateUrlDomain: validateUrlDomain, 2238 | validateUrlDomainSegment: validateUrlDomainSegment, 2239 | validateUrlDomainTld: validateUrlDomainTld, 2240 | validateUrlFragment: validateUrlFragment, 2241 | validateUrlHost: validateUrlHost, 2242 | validateUrlIp: validateUrlIp, 2243 | validateUrlIpv4: validateUrlIpv4, 2244 | validateUrlIpv6: validateUrlIpv6, 2245 | validateUrlPath: validateUrlPath, 2246 | validateUrlPchar: validateUrlPchar, 2247 | validateUrlPctEncoded: validateUrlPctEncoded, 2248 | validateUrlPort: validateUrlPort, 2249 | validateUrlQuery: validateUrlQuery, 2250 | validateUrlScheme: validateUrlScheme, 2251 | validateUrlSubDelims: validateUrlSubDelims, 2252 | validateUrlSubDomainSegment: validateUrlSubDomainSegment, 2253 | validateUrlUnencoded: validateUrlUnencoded, 2254 | validateUrlUnicodeAuthority: validateUrlUnicodeAuthority, 2255 | validateUrlUnicodeDomain: validateUrlUnicodeDomain, 2256 | validateUrlUnicodeDomainSegment: validateUrlUnicodeDomainSegment, 2257 | validateUrlUnicodeDomainTld: validateUrlUnicodeDomainTld, 2258 | validateUrlUnicodeHost: validateUrlUnicodeHost, 2259 | validateUrlUnicodeSubDomainSegment: validateUrlUnicodeSubDomainSegment, 2260 | validateUrlUnreserved: validateUrlUnreserved, 2261 | validateUrlUserinfo: validateUrlUserinfo, 2262 | validCashtag: validCashtag, 2263 | validCCTLD: validCCTLD, 2264 | validDomain: validDomain, 2265 | validDomainChars: validDomainChars, 2266 | validDomainName: validDomainName, 2267 | validGeneralUrlPathChars: validGeneralUrlPathChars, 2268 | validGTLD: validGTLD, 2269 | validHashtag: validHashtag, 2270 | validMentionOrList: validMentionOrList, 2271 | validMentionPrecedingChars: validMentionPrecedingChars, 2272 | validPortNumber: validPortNumber, 2273 | validPunycode: validPunycode, 2274 | validReply: validReply, 2275 | validSpecialCCTLD: validSpecialCCTLD, 2276 | validSpecialShortDomain: validSpecialShortDomain, 2277 | validSubdomain: validSubdomain, 2278 | validTcoUrl: validTcoUrl, 2279 | validUrlBalancedParens: validUrlBalancedParens, 2280 | validUrlPath: validUrlPath, 2281 | validUrlPathEndingChars: validUrlPathEndingChars, 2282 | validUrlPrecedingChars: validUrlPrecedingChars, 2283 | validUrlQueryChars: validUrlQueryChars, 2284 | validUrlQueryEndingChars: validUrlQueryEndingChars 2285 | }; 2286 | 2287 | var index = { 2288 | autoLink: autoLink, 2289 | autoLinkCashtags: autoLinkCashtags, 2290 | autoLinkEntities: autoLinkEntities, 2291 | autoLinkHashtags: autoLinkHashtags, 2292 | autoLinkUrlsCustom: autoLinkUrlsCustom, 2293 | autoLinkUsernamesOrLists: autoLinkUsernamesOrLists, 2294 | autoLinkWithJSON: autoLinkWithJSON, 2295 | configs: configs, 2296 | convertUnicodeIndices: convertUnicodeIndices$2, 2297 | extractCashtags: extractCashtags, 2298 | extractCashtagsWithIndices: extractCashtagsWithIndices, 2299 | extractEntitiesWithIndices: extractEntitiesWithIndices, 2300 | extractHashtags: extractHashtags, 2301 | extractHashtagsWithIndices: extractHashtagsWithIndices, 2302 | extractHtmlAttrsFromOptions: extractHtmlAttrsFromOptions, 2303 | extractMentions: extractMentions, 2304 | extractMentionsOrListsWithIndices: extractMentionsOrListsWithIndices, 2305 | extractMentionsWithIndices: extractMentionsWithIndices, 2306 | extractReplies: extractReplies, 2307 | extractUrls: extractUrls, 2308 | extractUrlsWithIndices: extractUrlsWithIndices, 2309 | getTweetLength: getTweetLength, 2310 | getUnicodeTextLength: getUnicodeTextLength, 2311 | hasInvalidCharacters: hasInvalidCharacters, 2312 | hitHighlight: hitHighlight, 2313 | htmlEscape: htmlEscape, 2314 | isInvalidTweet: isInvalidTweet, 2315 | isValidHashtag: isValidHashtag, 2316 | isValidList: isValidList, 2317 | isValidTweetText: isValidTweetText, 2318 | isValidUrl: isValidUrl$1, 2319 | isValidUsername: isValidUsername, 2320 | linkTextWithEntity: linkTextWithEntity, 2321 | linkToCashtag: linkToCashtag, 2322 | linkToHashtag: linkToHashtag, 2323 | linkToMentionAndList: linkToMentionAndList, 2324 | linkToText: linkToText, 2325 | linkToTextWithSymbol: linkToTextWithSymbol, 2326 | linkToUrl: linkToUrl, 2327 | modifyIndicesFromUTF16ToUnicode: modifyIndicesFromUTF16ToUnicode, 2328 | modifyIndicesFromUnicodeToUTF16: modifyIndicesFromUnicodeToUTF16, 2329 | regexen: regexen, 2330 | removeOverlappingEntities: removeOverlappingEntities, 2331 | parseTweet: parseTweet, 2332 | splitTags: splitTags, 2333 | tagAttrs: tagAttrs 2334 | }; 2335 | 2336 | return index; 2337 | 2338 | }))); 2339 | --------------------------------------------------------------------------------