├── .gitignore ├── test ├── .DS_Store ├── test.html ├── tests.js └── conformance.html ├── .gitmodules ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── qunit.css └── qunit.js ├── twitter-text.js └── pkg ├── twitter-text-1.3.1.js ├── twitter-text-1.0.0.js ├── twitter-text-1.0.3.js ├── twitter-text-1.0.1.js ├── twitter-text-1.0.4.js ├── twitter-text-1.0.2.js └── twitter-text-1.0.5.js /.gitignore: -------------------------------------------------------------------------------- 1 | test/conformance.js 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/twitter-text-js/HEAD/test/.DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/twitter-text-conformance"] 2 | path = test/twitter-text-conformance 3 | url = git://github.com/mzsanford/twitter-text-conformance.git 4 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

twitter-text-js

15 |

16 |

17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 Twitter, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 | use this file except in compliance with the License. You may obtain a copy of 5 | the License 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, WITHOUT 11 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | License for the specific language governing permissions and limitations under 13 | the License. 14 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | module("twttr.txt"); 2 | 3 | test("twttr.txt.htmlEscape", function() { 4 | var tests = [ 5 | ["&", "&"], 6 | [">", ">"], 7 | ["<", "<"], 8 | ["\"", """], 9 | ["'", " "], 10 | ["&<>\"", "&<>""], 11 | ["
    ", "<div>"], 12 | ["a&b", "a&b"], 13 | ["twitter & friends", "<a href="http://twitter.com" target="_blank">twitter & friends</a>"], 14 | ["&", "&amp;"], 15 | [undefined, undefined, "calling with undefined will return input"] 16 | ]; 17 | 18 | for (var i = 0; i < tests.length; i++) { 19 | same(twttr.txt.htmlEscape(tests[i][0]), tests[i][1], tests[i][2] || tests[i][0]); 20 | } 21 | }); 22 | 23 | test("twttr.txt.splitTags", function() { 24 | var tests = [ 25 | ["foo", ["foo"]], 26 | ["foofoo", ["foo", "a", "foo"]], 27 | ["<><>", ["", "", "", "", ""]], 28 | ["foo", ["", "a", "", "em", "foo", "/em", "", "/a", ""]] 29 | ]; 30 | 31 | for (var i = 0; i < tests.length; i++) { 32 | same(twttr.txt.splitTags(tests[i][0]), tests[i][1], tests[i][2] || tests[i][0]); 33 | } 34 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twitter-text-js 2 | 3 | A JavaScript library that provides text processing routines for Tweets. This library conforms to a common test suite shared by many other implementations, particularly twitter-text.gem (Ruby). The library provides autolinking and extraction for URLs, usernames, lists, and hashtags. 4 | 5 | ## Extraction Examples 6 | 7 | // basic extraction 8 | var usernames = twttr.txt.extract_mentioned_screen_names("Mentioning @twitter and @jack") 9 | // usernames == ["twitter", "jack"] 10 | 11 | ## Auto-linking Examples 12 | 13 | twttr.txt.autoLink("link @user, please #request"); 14 | 15 | ## Usernames 16 | 17 | Username extraction and linking matches all valid Twitter usernames but does 18 | not verify that the username is a valid Twitter account. 19 | 20 | ## Lists 21 | 22 | Auto-link and extract list names when they are written in @user/list-name 23 | format. 24 | 25 | ## Hashtags 26 | 27 | Auto-link and extract hashtags, where a hashtag contains any latin letter or 28 | number but cannot be solely numbers. 29 | 30 | ## URLs 31 | 32 | Auto-linking and extraction of URLs differs from some other linkification libraries so that they 33 | will work correctly in Tweets written in languages that do not include spaces 34 | between words. 35 | 36 | ## International 37 | 38 | Special care has been taken to be sure that auto-linking and extraction work 39 | in Tweets of all languages. This means that languages without spaces between 40 | words should work equally well. 41 | 42 | ## Hit Highlighting 43 | 44 | Use to provide emphasis around the "hits" returned from the Search API, built 45 | to work against text that has been auto-linked already. 46 | 47 | ## Testing 48 | 49 | ### Conformance 50 | 51 | The main test suite is twitter-text-conformance. This is set up as a git submodule that is automatically updated at each run. Tests are run in your browser, using QUnit. To run the conformance suite, from the project root, run: 52 | 53 | rake test:conformance 54 | 55 | Your default browser will open the test suite. You should open the test suite in your other browsers as you see fit. 56 | 57 | ### Other Tests 58 | 59 | There are a few tests specific to twitter-text-js that are not part of the conformance suite. To run these, from the project root, run: 60 | 61 | rake test 62 | 63 | Your default browser will open the test suite. 64 | 65 | ## Packaging 66 | 67 | Official versions are kept in the `pkg/` directory. To roll a new version, (ex. v1.1.0), run the following from project root: 68 | 69 | rake package[1.1.0] 70 | 71 | This will make a new file at `pkg/twitter-text-1.1.0.js`. 72 | 73 | ## Reporting Bugs 74 | 75 | Please direct bug reports to the [twitter-text-js issue tracker on GitHub](http://github.com/bcherry/twitter-text-js/issues) -------------------------------------------------------------------------------- /test/conformance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 83 | 84 | 85 | 86 |

    Twitter Text Conformance Suite

    87 |

    88 |

    89 |
      90 | 91 | 92 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'yaml' 3 | require 'json' 4 | require 'date' 5 | require 'digest' 6 | 7 | def conformance_version(dir) 8 | Dir[File.join(dir, '*')].inject(Digest::SHA1.new){|digest, file| digest.update(Digest::SHA1.file(file).hexdigest) } 9 | end 10 | 11 | namespace :test do 12 | namespace :conformance do 13 | desc "Update conformance testing data" 14 | task :update do 15 | puts "Updating conformance data ... " 16 | system("git submodule init") || raise("Failed to init submodule") 17 | system("git submodule update") || raise("Failed to update submodule") 18 | puts "Updating conformance data ... DONE" 19 | end 20 | 21 | desc "Change conformance test data to the lastest version" 22 | task :latest => ['conformance:update'] do 23 | current_dir = File.dirname(__FILE__) 24 | submodule_dir = File.join(File.dirname(__FILE__), "test", "twitter-text-conformance") 25 | version_before = conformance_version(submodule_dir) 26 | system("cd #{submodule_dir} && git pull origin master") || raise("Failed to pull submodule version") 27 | system("cd #{current_dir}") 28 | if conformance_version(submodule_dir) != version_before 29 | system("cd #{current_dir} && git add #{submodule_dir}") || raise("Failed to add upgrade files") 30 | system("git commit -m \"Upgraded to the latest conformance suite\" #{submodule_dir}") || raise("Failed to commit upgraded conformacne data") 31 | puts "Upgraded conformance suite." 32 | else 33 | puts "No conformance suite changes." 34 | end 35 | end 36 | 37 | desc "Prepare JS conformance test suite" 38 | task :prepare do 39 | test_files = ['autolink', 'extract', 'hit_highlighting'] 40 | r = {} 41 | 42 | f = File.open(File.join(File.dirname(__FILE__), "test", "conformance.js"), "w") 43 | f.write("var cases = {};") 44 | 45 | test_files.each do |test_file| 46 | path = File.join(File.dirname(__FILE__), "test", "twitter-text-conformance", test_file + ".yml") 47 | yml = YAML.load_file(path) 48 | f.write("cases.#{test_file} = #{yml['tests'].to_json};") 49 | end 50 | 51 | f.close 52 | end 53 | 54 | desc "Run conformance test suite" 55 | task :run do 56 | exec('open test/conformance.html') 57 | end 58 | end 59 | 60 | desc "Run conformance test suite" 61 | task :conformance => ['conformance:latest', 'conformance:prepare', 'conformance:run'] do 62 | end 63 | 64 | desc "Run JavaScript test suite" 65 | task :run do 66 | exec('open test/test.html') 67 | end 68 | end 69 | 70 | desc "Run JavaScript test suite" 71 | task :test => ['test:run'] 72 | 73 | directory "pkg" 74 | 75 | task :package, [:version] => [:pkg] do |t, args| 76 | pkg_name = "twitter-text-#{args.version}.js" 77 | puts "Building #{pkg_name}..." 78 | 79 | lib_name = "twitter-text-js #{args.version}" 80 | 81 | pkg_file = File.open(File.join(File.dirname(__FILE__), "pkg", pkg_name), "w") 82 | pkg_file.write("/*!\n * #{lib_name}\n *\n") 83 | 84 | puts "Writing license..." 85 | license_file = File.open(File.join(File.dirname(__FILE__), "LICENSE"), "r") 86 | license_file.each_line do |line| 87 | pkg_file.write(" * #{line}") 88 | end 89 | license_file.close 90 | 91 | pkg_file.write(" */\n\n") 92 | 93 | puts "Writing library..." 94 | js_file = File.open(File.join(File.dirname(__FILE__), "twitter-text.js"), "r") 95 | pkg_file.write(js_file.read) 96 | js_file.close 97 | 98 | pkg_file.close 99 | 100 | puts "Done with #{pkg_name}" 101 | 102 | end -------------------------------------------------------------------------------- /lib/qunit.css: -------------------------------------------------------------------------------- 1 | /** Font Family and Sizes */ 2 | 3 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 4 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 5 | } 6 | 7 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 8 | #qunit-tests { font-size: smaller; } 9 | 10 | 11 | /** Resets */ 12 | 13 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | 19 | /** Header */ 20 | 21 | #qunit-header { 22 | padding: 0.5em 0 0.5em 1em; 23 | 24 | color: #fff; 25 | text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; 26 | background-color: #0d3349; 27 | 28 | border-radius: 15px 15px 0 0; 29 | -moz-border-radius: 15px 15px 0 0; 30 | -webkit-border-top-right-radius: 15px; 31 | -webkit-border-top-left-radius: 15px; 32 | } 33 | 34 | #qunit-header a { 35 | text-decoration: none; 36 | color: white; 37 | } 38 | 39 | #qunit-banner { 40 | height: 5px; 41 | } 42 | 43 | #qunit-testrunner-toolbar { 44 | padding: 0em 0 0.5em 2em; 45 | } 46 | 47 | #qunit-userAgent { 48 | padding: 0.5em 0 0.5em 2.5em; 49 | background-color: #2b81af; 50 | color: #fff; 51 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 52 | } 53 | 54 | 55 | /** Tests: Pass/Fail */ 56 | 57 | #qunit-tests { 58 | list-style-position: inside; 59 | } 60 | 61 | #qunit-tests li { 62 | padding: 0.4em 0.5em 0.4em 2.5em; 63 | border-bottom: 1px solid #fff; 64 | list-style-position: inside; 65 | } 66 | 67 | #qunit-tests li strong { 68 | cursor: pointer; 69 | } 70 | 71 | #qunit-tests ol { 72 | margin-top: 0.5em; 73 | padding: 0.5em; 74 | 75 | background-color: #fff; 76 | 77 | border-radius: 15px; 78 | -moz-border-radius: 15px; 79 | -webkit-border-radius: 15px; 80 | 81 | box-shadow: inset 0px 2px 13px #999; 82 | -moz-box-shadow: inset 0px 2px 13px #999; 83 | -webkit-box-shadow: inset 0px 2px 13px #999; 84 | } 85 | 86 | /*** Test Counts */ 87 | 88 | #qunit-tests b.counts { color: black; } 89 | #qunit-tests b.passed { color: #5E740B; } 90 | #qunit-tests b.failed { color: #710909; } 91 | 92 | #qunit-tests li li { 93 | margin: 0.5em; 94 | padding: 0.4em 0.5em 0.4em 0.5em; 95 | background-color: #fff; 96 | border-bottom: none; 97 | list-style-position: inside; 98 | } 99 | 100 | /*** Passing Styles */ 101 | 102 | #qunit-tests li li.pass { 103 | color: #5E740B; 104 | background-color: #fff; 105 | border-left: 26px solid #C6E746; 106 | } 107 | 108 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 109 | #qunit-tests .pass .test-name { color: #366097; } 110 | 111 | #qunit-tests .pass .test-actual, 112 | #qunit-tests .pass .test-expected { color: #999999; } 113 | 114 | #qunit-banner.qunit-pass { background-color: #C6E746; } 115 | 116 | /*** Failing Styles */ 117 | 118 | #qunit-tests li li.fail { 119 | color: #710909; 120 | background-color: #fff; 121 | border-left: 26px solid #EE5757; 122 | } 123 | 124 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 125 | #qunit-tests .fail .test-name, 126 | #qunit-tests .fail .module-name { color: #000000; } 127 | 128 | #qunit-tests .fail .test-actual { color: #EE5757; } 129 | #qunit-tests .fail .test-expected { color: green; } 130 | 131 | #qunit-banner.qunit-fail, 132 | #qunit-testrunner-toolbar { background-color: #EE5757; } 133 | 134 | 135 | /** Footer */ 136 | 137 | #qunit-testresult { 138 | padding: 0.5em 0.5em 0.5em 2.5em; 139 | 140 | color: #2b81af; 141 | background-color: #D2E0E6; 142 | 143 | border-radius: 0 0 15px 15px; 144 | -moz-border-radius: 0 0 15px 15px; 145 | -webkit-border-bottom-right-radius: 15px; 146 | -webkit-border-bottom-left-radius: 15px; 147 | } 148 | 149 | /** Fixture */ 150 | 151 | #qunit-fixture { 152 | position: absolute; 153 | top: -10000px; 154 | left: -10000px; 155 | } 156 | -------------------------------------------------------------------------------- /twitter-text.js: -------------------------------------------------------------------------------- 1 | if (!window.twttr) { 2 | window.twttr = {}; 3 | } 4 | 5 | (function() { 6 | twttr.txt = {}; 7 | twttr.txt.regexen = {}; 8 | 9 | var HTML_ENTITIES = { 10 | '&': '&', 11 | '>': '>', 12 | '<': '<', 13 | '"': '"', 14 | "'": ' ' 15 | }; 16 | 17 | // HTML escaping 18 | twttr.txt.htmlEscape = function(text) { 19 | return text && text.replace(/[&"'><]/g, function(character) { 20 | return HTML_ENTITIES[character]; 21 | }); 22 | }; 23 | 24 | // Builds a RegExp 25 | function regexSupplant(regex, flags) { 26 | flags = flags || ""; 27 | if (typeof regex !== "string") { 28 | if (regex.global && flags.indexOf("g") < 0) { 29 | flags += "g"; 30 | } 31 | if (regex.ignoreCase && flags.indexOf("i") < 0) { 32 | flags += "i"; 33 | } 34 | if (regex.multiline && flags.indexOf("m") < 0) { 35 | flags += "m"; 36 | } 37 | 38 | regex = regex.source; 39 | } 40 | 41 | return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { 42 | var newRegex = twttr.txt.regexen[name] || ""; 43 | if (typeof newRegex !== "string") { 44 | newRegex = newRegex.source; 45 | } 46 | return newRegex; 47 | }), flags); 48 | } 49 | 50 | // simple string interpolation 51 | function stringSupplant(str, values) { 52 | return str.replace(/#\{(\w+)\}/g, function(match, name) { 53 | return values[name] || ""; 54 | }); 55 | } 56 | 57 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand 58 | // to access both the list of characters and a pattern suitible for use with String#split 59 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE 60 | var fromCode = String.fromCharCode; 61 | var UNICODE_SPACES = [ 62 | fromCode(0x0020), // White_Space # Zs SPACE 63 | fromCode(0x0085), // White_Space # Cc 64 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE 65 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK 66 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR 67 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR 68 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR 69 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE 70 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE 71 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE 72 | ]; 73 | 74 | for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] .. 75 | UNICODE_SPACES.push(String.fromCharCode(i)); 76 | } 77 | 78 | for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE 79 | UNICODE_SPACES.push(String.fromCharCode(i)); 80 | } 81 | 82 | twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]"); 83 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; 84 | twttr.txt.regexen.atSigns = /[@@]/; 85 | twttr.txt.regexen.extractMentions = regexSupplant(/(^|[^a-zA-Z0-9_])(#{atSigns})([a-zA-Z0-9_]{1,20})(?=(.|$))/g); 86 | twttr.txt.regexen.extractReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); 87 | twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; 88 | 89 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") 90 | twttr.txt.regexen.latinAccentChars = regexSupplant("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277"); 91 | twttr.txt.regexen.latenAccents = regexSupplant(/[#{latinAccentChars}]+/); 92 | 93 | twttr.txt.regexen.endScreenNameMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/); 94 | 95 | // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid. 96 | twttr.txt.regexen.hashtagCharacters = regexSupplant(/[a-z0-9_#{latinAccentChars}]/i); 97 | twttr.txt.regexen.autoLinkHashtags = regexSupplant(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi); 98 | twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g; 99 | twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; 100 | 101 | // URL related hash regex collection 102 | twttr.txt.regexen.validPrecedingChars = regexSupplant(/(?:[^-\/"':!=A-Za-z0-9_@@]|^|\:)/); 103 | twttr.txt.regexen.validDomain = regexSupplant(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i); 104 | 105 | twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i; 106 | // Allow URL paths to contain balanced parens 107 | // 1. Used in Wikipedia URLs like /Primer_(film) 108 | // 2. Used in IIS sessions like /S(dfd346)/ 109 | twttr.txt.regexen.wikipediaDisambiguation = regexSupplant(/(?:\(#{validGeneralUrlPathChars}+\))/i); 110 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user 111 | twttr.txt.regexen.validUrlPathChars = regexSupplant(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.,]?#{validGeneralUrlPathChars})/i); 112 | 113 | // Valid end-of-path chracters (so /foo. does not gobble the period). 114 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 115 | twttr.txt.regexen.validUrlPathEndingChars = regexSupplant(/(?:[a-z0-9=_#\/]|#{wikipediaDisambiguation})/i); 116 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i; 117 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i; 118 | twttr.txt.regexen.validUrl = regexSupplant( 119 | '(' + // $1 total match 120 | '(#{validPrecedingChars})' + // $2 Preceeding chracter 121 | '(' + // $3 URL 122 | '(https?:\\/\\/)' + // $4 Protocol 123 | '(#{validDomain})' + // $5 Domain(s) and optional post number 124 | '(\\/' + // $6 URL Path 125 | '(?:' + 126 | '#{validUrlPathChars}+#{validUrlPathEndingChars}|' + 127 | '#{validUrlPathChars}+#{validUrlPathEndingChars}?|' + 128 | '#{validUrlPathEndingChars}' + 129 | ')?' + 130 | ')?' + 131 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String 132 | ')' + 133 | ')' 134 | , "gi"); 135 | 136 | // Default CSS class for auto-linked URLs 137 | var DEFAULT_URL_CLASS = "tweet-url"; 138 | // Default CSS class for auto-linked lists (along with the url class) 139 | var DEFAULT_LIST_CLASS = "list-slug"; 140 | // Default CSS class for auto-linked usernames (along with the url class) 141 | var DEFAULT_USERNAME_CLASS = "username"; 142 | // Default CSS class for auto-linked hashtags (along with the url class) 143 | var DEFAULT_HASHTAG_CLASS = "hashtag"; 144 | // HTML attribute for robot nofollow behavior (default) 145 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; 146 | 147 | // Simple object cloning function for simple objects 148 | function clone(o) { 149 | var r = {}; 150 | for (var k in o) { 151 | if (o.hasOwnProperty(k)) { 152 | r[k] = o[k]; 153 | } 154 | } 155 | 156 | return r; 157 | } 158 | 159 | twttr.txt.autoLink = function(text, options) { 160 | options = clone(options || {}); 161 | return twttr.txt.autoLinkUsernamesOrLists( 162 | twttr.txt.autoLinkUrlsCustom( 163 | twttr.txt.autoLinkHashtags(text, options), 164 | options), 165 | options); 166 | }; 167 | 168 | 169 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) { 170 | options = clone(options || {}); 171 | 172 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 173 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 174 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 175 | options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; 176 | options.listUrlBase = options.listUrlBase || "http://twitter.com/"; 177 | if (!options.suppressNoFollow) { 178 | var extraHtml = HTML_ATTR_NO_FOLLOW; 179 | } 180 | 181 | var newText = "", 182 | splitText = twttr.txt.splitTags(text); 183 | 184 | for (var index = 0; index < splitText.length; index++) { 185 | var chunk = splitText[index]; 186 | 187 | if (index !== 0) { 188 | newText += ((index % 2 === 0) ? ">" : "<"); 189 | } 190 | 191 | if (index % 4 !== 0) { 192 | newText += chunk; 193 | } else { 194 | newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) { 195 | var after = chunk.slice(offset + match.length); 196 | 197 | var d = { 198 | before: before, 199 | at: at, 200 | user: twttr.txt.htmlEscape(user), 201 | slashListname: twttr.txt.htmlEscape(slashListname), 202 | extraHtml: extraHtml, 203 | chunk: twttr.txt.htmlEscape(chunk) 204 | }; 205 | for (var k in options) { 206 | if (options.hasOwnProperty(k)) { 207 | d[k] = options[k]; 208 | } 209 | } 210 | 211 | if (slashListname && !options.suppressLists) { 212 | // the link is a list 213 | var list = d.chunk = stringSupplant("#{user}#{slashListname}", d); 214 | d.list = twttr.txt.htmlEscape(list.toLowerCase()); 215 | return stringSupplant("#{before}#{at}#{chunk}", d); 216 | } else { 217 | if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { 218 | // Followed by something that means we don't autolink 219 | return match; 220 | } else { 221 | // this is a screen name 222 | d.chunk = twttr.txt.htmlEscape(user); 223 | d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : ""; 224 | return stringSupplant("#{before}#{at}#{chunk}", d); 225 | } 226 | } 227 | }); 228 | } 229 | } 230 | 231 | return newText; 232 | }; 233 | 234 | twttr.txt.autoLinkHashtags = function(text, options) { 235 | options = clone(options || {}); 236 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 237 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 238 | options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; 239 | if (!options.suppressNoFollow) { 240 | var extraHtml = HTML_ATTR_NO_FOLLOW; 241 | } 242 | 243 | return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) { 244 | var d = { 245 | before: before, 246 | hash: twttr.txt.htmlEscape(hash), 247 | text: twttr.txt.htmlEscape(text), 248 | extraHtml: extraHtml 249 | }; 250 | 251 | for (var k in options) { 252 | if (options.hasOwnProperty(k)) { 253 | d[k] = options[k]; 254 | } 255 | } 256 | 257 | return stringSupplant("#{before}#{hash}#{text}", d); 258 | }); 259 | }; 260 | 261 | 262 | twttr.txt.autoLinkUrlsCustom = function(text, options) { 263 | options = clone(options || {}); 264 | if (!options.suppressNoFollow) { 265 | options.rel = "nofollow"; 266 | } 267 | if (options.urlClass) { 268 | options["class"] = options.urlClass; 269 | delete options.urlClass; 270 | } 271 | 272 | delete options.suppressNoFollow; 273 | delete options.suppressDataScreenName; 274 | 275 | return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) { 276 | var tldComponents; 277 | 278 | if (protocol) { 279 | var htmlAttrs = ""; 280 | for (var k in options) { 281 | htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); 282 | } 283 | options.htmlAttrs || ""; 284 | 285 | var d = { 286 | before: before, 287 | htmlAttrs: htmlAttrs, 288 | url: twttr.txt.htmlEscape(url) 289 | }; 290 | 291 | return stringSupplant("#{before}#{url}", d); 292 | } else { 293 | return all; 294 | } 295 | }); 296 | }; 297 | 298 | twttr.txt.extractMentions = function(text) { 299 | var screenNamesOnly = [], 300 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); 301 | 302 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 303 | var screenName = screenNamesWithIndices[i].screenName; 304 | screenNamesOnly.push(screenName); 305 | } 306 | 307 | return screenNamesOnly; 308 | }; 309 | 310 | twttr.txt.extractMentionsWithIndices = function(text) { 311 | if (!text) { 312 | return []; 313 | } 314 | 315 | var possibleScreenNames = [], 316 | position = 0; 317 | 318 | text.replace(twttr.txt.regexen.extractMentions, function(match, before, atSign, screenName, after) { 319 | if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { 320 | var startPosition = text.indexOf(atSign + screenName, position); 321 | position = startPosition + screenName.length + 1; 322 | possibleScreenNames.push({ 323 | screenName: screenName, 324 | indices: [startPosition, position] 325 | }); 326 | } 327 | }); 328 | 329 | return possibleScreenNames; 330 | }; 331 | 332 | twttr.txt.extractReplies = function(text) { 333 | if (!text) { 334 | return null; 335 | } 336 | 337 | var possibleScreenName = text.match(twttr.txt.regexen.extractReply); 338 | if (!possibleScreenName) { 339 | return null; 340 | } 341 | 342 | return possibleScreenName[1]; 343 | }; 344 | 345 | twttr.txt.extractUrls = function(text) { 346 | var urlsOnly = [], 347 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); 348 | 349 | for (var i = 0; i < urlsWithIndices.length; i++) { 350 | urlsOnly.push(urlsWithIndices[i].url); 351 | } 352 | 353 | return urlsOnly; 354 | }; 355 | 356 | twttr.txt.extractUrlsWithIndices = function(text) { 357 | if (!text) { 358 | return []; 359 | } 360 | 361 | var urls = [], 362 | position = 0; 363 | 364 | text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) { 365 | var tldComponents; 366 | 367 | if (protocol) { 368 | var startPosition = text.indexOf(url, position), 369 | position = startPosition + url.length; 370 | 371 | urls.push({ 372 | url: url, 373 | indices: [startPosition, position] 374 | }); 375 | } 376 | }); 377 | 378 | return urls; 379 | }; 380 | 381 | twttr.txt.extractHashtags = function(text) { 382 | var hashtagsOnly = [], 383 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); 384 | 385 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 386 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 387 | } 388 | 389 | return hashtagsOnly; 390 | }; 391 | 392 | twttr.txt.extractHashtagsWithIndices = function(text) { 393 | if (!text) { 394 | return []; 395 | } 396 | 397 | var tags = [], 398 | position = 0; 399 | 400 | text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) { 401 | var startPosition = text.indexOf(hash + hashText, position); 402 | position = startPosition + hashText.length + 1; 403 | tags.push({ 404 | hashtag: hashText, 405 | indices: [startPosition, position] 406 | }); 407 | }); 408 | 409 | return tags; 410 | }; 411 | 412 | // this essentially does text.split(/<|>/) 413 | // except that won't work in IE, where empty strings are ommitted 414 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 415 | // but "<<".split("<") => ["", "", ""] 416 | twttr.txt.splitTags = function(text) { 417 | var firstSplits = text.split("<"), 418 | secondSplits, 419 | allSplits = [], 420 | split; 421 | 422 | for (var i = 0; i < firstSplits.length; i += 1) { 423 | split = firstSplits[i]; 424 | if (!split) { 425 | allSplits.push(""); 426 | } else { 427 | secondSplits = split.split(">"); 428 | for (var j = 0; j < secondSplits.length; j += 1) { 429 | allSplits.push(secondSplits[j]); 430 | } 431 | } 432 | } 433 | 434 | return allSplits; 435 | }; 436 | 437 | twttr.txt.hitHighlight = function(text, hits, options) { 438 | var defaultHighlightTag = "em"; 439 | 440 | hits = hits || []; 441 | options = options || {}; 442 | 443 | if (hits.length === 0) { 444 | return text; 445 | } 446 | 447 | var tagName = options.tag || defaultHighlightTag, 448 | tags = ["<" + tagName + ">", ""], 449 | chunks = twttr.txt.splitTags(text), 450 | split, 451 | i, 452 | j, 453 | result = "", 454 | chunkIndex = 0, 455 | chunk = chunks[0], 456 | prevChunksLen = 0, 457 | chunkCursor = 0, 458 | startInChunk = false, 459 | chunkChars = chunk, 460 | flatHits = [], 461 | index, 462 | hit, 463 | tag, 464 | placed, 465 | hitSpot; 466 | 467 | for (i = 0; i < hits.length; i += 1) { 468 | for (j = 0; j < hits[i].length; j += 1) { 469 | flatHits.push(hits[i][j]); 470 | } 471 | } 472 | 473 | for (index = 0; index < flatHits.length; index += 1) { 474 | hit = flatHits[index]; 475 | tag = tags[index % 2]; 476 | placed = false; 477 | 478 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 479 | result += chunkChars.slice(chunkCursor); 480 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 481 | result += tag; 482 | placed = true; 483 | } 484 | 485 | if (chunks[chunkIndex + 1]) { 486 | result += "<" + chunks[chunkIndex + 1] + ">"; 487 | } 488 | 489 | prevChunksLen += chunkChars.length; 490 | chunkCursor = 0; 491 | chunkIndex += 2; 492 | chunk = chunks[chunkIndex]; 493 | chunkChars = chunk; 494 | startInChunk = false; 495 | } 496 | 497 | if (!placed && chunk != null) { 498 | hitSpot = hit - prevChunksLen; 499 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 500 | chunkCursor = hitSpot; 501 | if (index % 2 === 0) { 502 | startInChunk = true; 503 | } else { 504 | startInChunk = false; 505 | } 506 | } else if(!placed) { 507 | placed = true; 508 | result += tag; 509 | } 510 | } 511 | 512 | if (chunk != null) { 513 | if (chunkCursor < chunkChars.length) { 514 | result += chunkChars.slice(chunkCursor); 515 | } 516 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 517 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); 518 | } 519 | } 520 | 521 | return result; 522 | }; 523 | 524 | 525 | }()); -------------------------------------------------------------------------------- /pkg/twitter-text-1.3.1.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * twitter-text-js 1.3.1 3 | * 4 | * Copyright 2010 Twitter, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | * use this file except in compliance with the License. You may obtain a copy of 8 | * the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations under 16 | * the License. 17 | */ 18 | 19 | if (!window.twttr) { 20 | window.twttr = {}; 21 | } 22 | 23 | (function() { 24 | twttr.txt = {}; 25 | twttr.txt.regexen = {}; 26 | 27 | var HTML_ENTITIES = { 28 | '&': '&', 29 | '>': '>', 30 | '<': '<', 31 | '"': '"', 32 | "'": ' ' 33 | }; 34 | 35 | // HTML escaping 36 | twttr.txt.htmlEscape = function(text) { 37 | return text && text.replace(/[&"'><]/g, function(character) { 38 | return HTML_ENTITIES[character]; 39 | }); 40 | }; 41 | 42 | // Builds a RegExp 43 | function regexSupplant(regex, flags) { 44 | flags = flags || ""; 45 | if (typeof regex !== "string") { 46 | if (regex.global && flags.indexOf("g") < 0) { 47 | flags += "g"; 48 | } 49 | if (regex.ignoreCase && flags.indexOf("i") < 0) { 50 | flags += "i"; 51 | } 52 | if (regex.multiline && flags.indexOf("m") < 0) { 53 | flags += "m"; 54 | } 55 | 56 | regex = regex.source; 57 | } 58 | 59 | return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { 60 | var newRegex = twttr.txt.regexen[name] || ""; 61 | if (typeof newRegex !== "string") { 62 | newRegex = newRegex.source; 63 | } 64 | return newRegex; 65 | }), flags); 66 | } 67 | 68 | // simple string interpolation 69 | function stringSupplant(str, values) { 70 | return str.replace(/#\{(\w+)\}/g, function(match, name) { 71 | return values[name] || ""; 72 | }); 73 | } 74 | 75 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand 76 | // to access both the list of characters and a pattern suitible for use with String#split 77 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE 78 | var fromCode = String.fromCharCode; 79 | var UNICODE_SPACES = [ 80 | fromCode(0x0020), // White_Space # Zs SPACE 81 | fromCode(0x0085), // White_Space # Cc 82 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE 83 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK 84 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR 85 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR 86 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR 87 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE 88 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE 89 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE 90 | ]; 91 | 92 | for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] .. 93 | UNICODE_SPACES.push(String.fromCharCode(i)); 94 | } 95 | 96 | for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE 97 | UNICODE_SPACES.push(String.fromCharCode(i)); 98 | } 99 | 100 | twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]"); 101 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; 102 | twttr.txt.regexen.atSigns = /[@@]/; 103 | twttr.txt.regexen.extractMentions = regexSupplant(/(^|[^a-zA-Z0-9_])(#{atSigns})([a-zA-Z0-9_]{1,20})(?=(.|$))/g); 104 | twttr.txt.regexen.extractReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); 105 | twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; 106 | 107 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") 108 | twttr.txt.regexen.latinAccentChars = regexSupplant("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277"); 109 | twttr.txt.regexen.latenAccents = regexSupplant(/[#{latinAccentChars}]+/); 110 | 111 | twttr.txt.regexen.endScreenNameMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/); 112 | 113 | // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid. 114 | twttr.txt.regexen.hashtagCharacters = regexSupplant(/[a-z0-9_#{latinAccentChars}]/i); 115 | twttr.txt.regexen.autoLinkHashtags = regexSupplant(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi); 116 | twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g; 117 | twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; 118 | 119 | // URL related hash regex collection 120 | twttr.txt.regexen.validPrecedingChars = regexSupplant(/(?:[^-\/"':!=A-Za-z0-9_@@]|^|\:)/); 121 | twttr.txt.regexen.validDomain = regexSupplant(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i); 122 | 123 | twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i; 124 | // Allow URL paths to contain balanced parens 125 | // 1. Used in Wikipedia URLs like /Primer_(film) 126 | // 2. Used in IIS sessions like /S(dfd346)/ 127 | twttr.txt.regexen.wikipediaDisambiguation = regexSupplant(/(?:\(#{validGeneralUrlPathChars}+\))/i); 128 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user 129 | twttr.txt.regexen.validUrlPathChars = regexSupplant(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.,]?#{validGeneralUrlPathChars})/i); 130 | 131 | // Valid end-of-path chracters (so /foo. does not gobble the period). 132 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 133 | twttr.txt.regexen.validUrlPathEndingChars = regexSupplant(/(?:[a-z0-9=_#\/]|#{wikipediaDisambiguation})/i); 134 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i; 135 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i; 136 | twttr.txt.regexen.validUrl = regexSupplant( 137 | '(' + // $1 total match 138 | '(#{validPrecedingChars})' + // $2 Preceeding chracter 139 | '(' + // $3 URL 140 | '(https?:\\/\\/)' + // $4 Protocol 141 | '(#{validDomain})' + // $5 Domain(s) and optional post number 142 | '(\\/' + // $6 URL Path 143 | '(?:' + 144 | '#{validUrlPathChars}+#{validUrlPathEndingChars}|' + 145 | '#{validUrlPathChars}+#{validUrlPathEndingChars}?|' + 146 | '#{validUrlPathEndingChars}' + 147 | ')?' + 148 | ')?' + 149 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String 150 | ')' + 151 | ')' 152 | , "gi"); 153 | 154 | // Default CSS class for auto-linked URLs 155 | var DEFAULT_URL_CLASS = "tweet-url"; 156 | // Default CSS class for auto-linked lists (along with the url class) 157 | var DEFAULT_LIST_CLASS = "list-slug"; 158 | // Default CSS class for auto-linked usernames (along with the url class) 159 | var DEFAULT_USERNAME_CLASS = "username"; 160 | // Default CSS class for auto-linked hashtags (along with the url class) 161 | var DEFAULT_HASHTAG_CLASS = "hashtag"; 162 | // HTML attribute for robot nofollow behavior (default) 163 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; 164 | 165 | // Simple object cloning function for simple objects 166 | function clone(o) { 167 | var r = {}; 168 | for (var k in o) { 169 | if (o.hasOwnProperty(k)) { 170 | r[k] = o[k]; 171 | } 172 | } 173 | 174 | return r; 175 | } 176 | 177 | twttr.txt.autoLink = function(text, options) { 178 | options = clone(options || {}); 179 | return twttr.txt.autoLinkUsernamesOrLists( 180 | twttr.txt.autoLinkUrlsCustom( 181 | twttr.txt.autoLinkHashtags(text, options), 182 | options), 183 | options); 184 | }; 185 | 186 | 187 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) { 188 | options = clone(options || {}); 189 | 190 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 191 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 192 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 193 | options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; 194 | options.listUrlBase = options.listUrlBase || "http://twitter.com/"; 195 | if (!options.suppressNoFollow) { 196 | var extraHtml = HTML_ATTR_NO_FOLLOW; 197 | } 198 | 199 | var newText = "", 200 | splitText = twttr.txt.splitTags(text); 201 | 202 | for (var index = 0; index < splitText.length; index++) { 203 | var chunk = splitText[index]; 204 | 205 | if (index !== 0) { 206 | newText += ((index % 2 === 0) ? ">" : "<"); 207 | } 208 | 209 | if (index % 4 !== 0) { 210 | newText += chunk; 211 | } else { 212 | newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) { 213 | var after = chunk.slice(offset + match.length); 214 | 215 | var d = { 216 | before: before, 217 | at: at, 218 | user: twttr.txt.htmlEscape(user), 219 | slashListname: twttr.txt.htmlEscape(slashListname), 220 | extraHtml: extraHtml, 221 | chunk: twttr.txt.htmlEscape(chunk) 222 | }; 223 | for (var k in options) { 224 | if (options.hasOwnProperty(k)) { 225 | d[k] = options[k]; 226 | } 227 | } 228 | 229 | if (slashListname && !options.suppressLists) { 230 | // the link is a list 231 | var list = d.chunk = stringSupplant("#{user}#{slashListname}", d); 232 | d.list = twttr.txt.htmlEscape(list.toLowerCase()); 233 | return stringSupplant("#{before}#{at}#{chunk}", d); 234 | } else { 235 | if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { 236 | // Followed by something that means we don't autolink 237 | return match; 238 | } else { 239 | // this is a screen name 240 | d.chunk = twttr.txt.htmlEscape(user); 241 | d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : ""; 242 | return stringSupplant("#{before}#{at}#{chunk}", d); 243 | } 244 | } 245 | }); 246 | } 247 | } 248 | 249 | return newText; 250 | }; 251 | 252 | twttr.txt.autoLinkHashtags = function(text, options) { 253 | options = clone(options || {}); 254 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 255 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 256 | options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; 257 | if (!options.suppressNoFollow) { 258 | var extraHtml = HTML_ATTR_NO_FOLLOW; 259 | } 260 | 261 | return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) { 262 | var d = { 263 | before: before, 264 | hash: twttr.txt.htmlEscape(hash), 265 | text: twttr.txt.htmlEscape(text), 266 | extraHtml: extraHtml 267 | }; 268 | 269 | for (var k in options) { 270 | if (options.hasOwnProperty(k)) { 271 | d[k] = options[k]; 272 | } 273 | } 274 | 275 | return stringSupplant("#{before}#{hash}#{text}", d); 276 | }); 277 | }; 278 | 279 | 280 | twttr.txt.autoLinkUrlsCustom = function(text, options) { 281 | options = clone(options || {}); 282 | if (!options.suppressNoFollow) { 283 | options.rel = "nofollow"; 284 | } 285 | if (options.urlClass) { 286 | options["class"] = options.urlClass; 287 | delete options.urlClass; 288 | } 289 | 290 | delete options.suppressNoFollow; 291 | delete options.suppressDataScreenName; 292 | 293 | return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) { 294 | var tldComponents; 295 | 296 | if (protocol) { 297 | var htmlAttrs = ""; 298 | for (var k in options) { 299 | htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); 300 | } 301 | options.htmlAttrs || ""; 302 | 303 | var d = { 304 | before: before, 305 | htmlAttrs: htmlAttrs, 306 | url: twttr.txt.htmlEscape(url) 307 | }; 308 | 309 | return stringSupplant("#{before}#{url}", d); 310 | } else { 311 | return all; 312 | } 313 | }); 314 | }; 315 | 316 | twttr.txt.extractMentions = function(text) { 317 | var screenNamesOnly = [], 318 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); 319 | 320 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 321 | var screenName = screenNamesWithIndices[i].screenName; 322 | screenNamesOnly.push(screenName); 323 | } 324 | 325 | return screenNamesOnly; 326 | }; 327 | 328 | twttr.txt.extractMentionsWithIndices = function(text) { 329 | if (!text) { 330 | return []; 331 | } 332 | 333 | var possibleScreenNames = [], 334 | position = 0; 335 | 336 | text.replace(twttr.txt.regexen.extractMentions, function(match, before, atSign, screenName, after) { 337 | if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { 338 | var startPosition = text.indexOf(atSign + screenName, position); 339 | position = startPosition + screenName.length + 1; 340 | possibleScreenNames.push({ 341 | screenName: screenName, 342 | indices: [startPosition, position] 343 | }); 344 | } 345 | }); 346 | 347 | return possibleScreenNames; 348 | }; 349 | 350 | twttr.txt.extractReplies = function(text) { 351 | if (!text) { 352 | return null; 353 | } 354 | 355 | var possibleScreenName = text.match(twttr.txt.regexen.extractReply); 356 | if (!possibleScreenName) { 357 | return null; 358 | } 359 | 360 | return possibleScreenName[1]; 361 | }; 362 | 363 | twttr.txt.extractUrls = function(text) { 364 | var urlsOnly = [], 365 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); 366 | 367 | for (var i = 0; i < urlsWithIndices.length; i++) { 368 | urlsOnly.push(urlsWithIndices[i].url); 369 | } 370 | 371 | return urlsOnly; 372 | }; 373 | 374 | twttr.txt.extractUrlsWithIndices = function(text) { 375 | if (!text) { 376 | return []; 377 | } 378 | 379 | var urls = [], 380 | position = 0; 381 | 382 | text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) { 383 | var tldComponents; 384 | 385 | if (protocol) { 386 | var startPosition = text.indexOf(url, position), 387 | position = startPosition + url.length; 388 | 389 | urls.push({ 390 | url: url, 391 | indices: [startPosition, position] 392 | }); 393 | } 394 | }); 395 | 396 | return urls; 397 | }; 398 | 399 | twttr.txt.extractHashtags = function(text) { 400 | var hashtagsOnly = [], 401 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); 402 | 403 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 404 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 405 | } 406 | 407 | return hashtagsOnly; 408 | }; 409 | 410 | twttr.txt.extractHashtagsWithIndices = function(text) { 411 | if (!text) { 412 | return []; 413 | } 414 | 415 | var tags = [], 416 | position = 0; 417 | 418 | text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) { 419 | var startPosition = text.indexOf(hash + hashText, position); 420 | position = startPosition + hashText.length + 1; 421 | tags.push({ 422 | hashtag: hashText, 423 | indices: [startPosition, position] 424 | }); 425 | }); 426 | 427 | return tags; 428 | }; 429 | 430 | // this essentially does text.split(/<|>/) 431 | // except that won't work in IE, where empty strings are ommitted 432 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 433 | // but "<<".split("<") => ["", "", ""] 434 | twttr.txt.splitTags = function(text) { 435 | var firstSplits = text.split("<"), 436 | secondSplits, 437 | allSplits = [], 438 | split; 439 | 440 | for (var i = 0; i < firstSplits.length; i += 1) { 441 | split = firstSplits[i]; 442 | if (!split) { 443 | allSplits.push(""); 444 | } else { 445 | secondSplits = split.split(">"); 446 | for (var j = 0; j < secondSplits.length; j += 1) { 447 | allSplits.push(secondSplits[j]); 448 | } 449 | } 450 | } 451 | 452 | return allSplits; 453 | }; 454 | 455 | twttr.txt.hitHighlight = function(text, hits, options) { 456 | var defaultHighlightTag = "em"; 457 | 458 | hits = hits || []; 459 | options = options || {}; 460 | 461 | if (hits.length === 0) { 462 | return text; 463 | } 464 | 465 | var tagName = options.tag || defaultHighlightTag, 466 | tags = ["<" + tagName + ">", ""], 467 | chunks = twttr.txt.splitTags(text), 468 | split, 469 | i, 470 | j, 471 | result = "", 472 | chunkIndex = 0, 473 | chunk = chunks[0], 474 | prevChunksLen = 0, 475 | chunkCursor = 0, 476 | startInChunk = false, 477 | chunkChars = chunk, 478 | flatHits = [], 479 | index, 480 | hit, 481 | tag, 482 | placed, 483 | hitSpot; 484 | 485 | for (i = 0; i < hits.length; i += 1) { 486 | for (j = 0; j < hits[i].length; j += 1) { 487 | flatHits.push(hits[i][j]); 488 | } 489 | } 490 | 491 | for (index = 0; index < flatHits.length; index += 1) { 492 | hit = flatHits[index]; 493 | tag = tags[index % 2]; 494 | placed = false; 495 | 496 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 497 | result += chunkChars.slice(chunkCursor); 498 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 499 | result += tag; 500 | placed = true; 501 | } 502 | 503 | if (chunks[chunkIndex + 1]) { 504 | result += "<" + chunks[chunkIndex + 1] + ">"; 505 | } 506 | 507 | prevChunksLen += chunkChars.length; 508 | chunkCursor = 0; 509 | chunkIndex += 2; 510 | chunk = chunks[chunkIndex]; 511 | chunkChars = chunk; 512 | startInChunk = false; 513 | } 514 | 515 | if (!placed && chunk != null) { 516 | hitSpot = hit - prevChunksLen; 517 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 518 | chunkCursor = hitSpot; 519 | if (index % 2 === 0) { 520 | startInChunk = true; 521 | } else { 522 | startInChunk = false; 523 | } 524 | } else if(!placed) { 525 | placed = true; 526 | result += tag; 527 | } 528 | } 529 | 530 | if (chunk != null) { 531 | if (chunkCursor < chunkChars.length) { 532 | result += chunkChars.slice(chunkCursor); 533 | } 534 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 535 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); 536 | } 537 | } 538 | 539 | return result; 540 | }; 541 | 542 | 543 | }()); -------------------------------------------------------------------------------- /pkg/twitter-text-1.0.0.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * twitter-text-js 1.0.0 3 | * 4 | * Copyright 2010 Twitter, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | * use this file except in compliance with the License. You may obtain a copy of 8 | * the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations under 16 | * the License. 17 | */ 18 | 19 | if (!window.twttr) { 20 | window.twttr = {}; 21 | } 22 | 23 | (function() { 24 | twttr.txt = {}; 25 | twttr.txt.regexen = {}; 26 | 27 | var HTML_ENTITIES = { 28 | '&': '&', 29 | '>': '>', 30 | '<': '<', 31 | '"': '"', 32 | "'": ' ' 33 | }; 34 | 35 | // HTML escaping 36 | twttr.txt.encode = function(text) { 37 | return text && text.replace(/[&"'><]/g, function(character) { 38 | return HTML_ENTITIES[character]; 39 | }); 40 | }; 41 | 42 | // Builds a RegExp 43 | function R(r, f) { 44 | f = f || ""; 45 | if (typeof r !== "string") { 46 | if (r.global && f.indexOf("g") < 0) { 47 | f += "g"; 48 | } 49 | if (r.ignoreCase && f.indexOf("i") < 0) { 50 | f += "i"; 51 | } 52 | if (r.multiline && f.indexOf("m") < 0) { 53 | f += "m"; 54 | } 55 | 56 | r = r.source; 57 | } 58 | 59 | return new RegExp(r.replace(/#\{(\w+)\}/g, function(m, name) { 60 | var regex = twttr.txt.regexen[name] || ""; 61 | if (typeof regex !== "string") { 62 | regex = regex.source; 63 | } 64 | return regex; 65 | }), f); 66 | } 67 | 68 | // simple string interpolation 69 | function S(s, d) { 70 | return s.replace(/#\{(\w+)\}/g, function(m, name) { 71 | return d[name] || ""; 72 | }); 73 | } 74 | 75 | // Join Regexes 76 | function J(a, f) { 77 | var r = ""; 78 | for (var i = 0; i < a.length; i++) { 79 | var s = a[i]; 80 | if (typeof s !== "string") { 81 | s = s.source; 82 | } 83 | r += s; 84 | } 85 | return new RegExp(r, f); 86 | } 87 | 88 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand 89 | // to access both the list of characters and a pattern suitible for use with String#split 90 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE 91 | var fromCode = String.fromCharCode; 92 | var UNICODE_SPACES = [ 93 | fromCode(0x0020), // White_Space # Zs SPACE 94 | fromCode(0x0085), // White_Space # Cc 95 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE 96 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK 97 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR 98 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR 99 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR 100 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE 101 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE 102 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE 103 | ]; 104 | 105 | for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] .. 106 | UNICODE_SPACES.push(String.fromCharCode(i)); 107 | } 108 | 109 | for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE 110 | UNICODE_SPACES.push(String.fromCharCode(i)); 111 | } 112 | 113 | twttr.txt.regexen.spaces = R("[" + UNICODE_SPACES.join("") + "]"); 114 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; 115 | twttr.txt.regexen.atSigns = /[@@]/; 116 | twttr.txt.regexen.extractMentions = R(/(^|[^a-zA-Z0-9_])#{atSigns}([a-zA-Z0-9_]{1,20})(?=(.|$))/g); 117 | twttr.txt.regexen.extractReply = R(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); 118 | twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; 119 | 120 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") 121 | var LATIN_ACCENTS = [ 122 | // (0xc0..0xd6).to_a, (0xd8..0xf6).to_a, (0xf8..0xff).to_a 123 | ];//.flatten.pack('U*').freeze 124 | twttr.txt.regexen.latinAccentChars = R("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277"); 125 | twttr.txt.regexen.latenAccents = R(/[#{latinAccentChars}]+/); 126 | 127 | twttr.txt.regexen.endScreenNameMatch = R(/#{atSigns}|[#{latinAccentChars}]/); 128 | 129 | // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid. 130 | twttr.txt.regexen.hashtagCharacters = R(/[a-z0-9_#{latinAccentChars}]/i); 131 | twttr.txt.regexen.autoLinkHashtags = R(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi); 132 | twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?(.|$)/g; 133 | twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; 134 | 135 | // URL related hash regex collection 136 | twttr.txt.regexen.validPrecedingChars = /(?:[^-\/"':!=A-Za-z0-9_]|^|\:)/; 137 | twttr.txt.regexen.validDomain = R(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i); 138 | 139 | // For protocol-less URLs, we'll accept them if they end in one of a handful of likely TLDs 140 | twttr.txt.regexen.probableTld = /\.(?:com|net|org|gov|edu)$/i; 141 | 142 | twttr.txt.regexen.www = /www\./i; 143 | 144 | twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i; 145 | // Allow URL paths to contain balanced parens 146 | // 1. Used in Wikipedia URLs like /Primer_(film) 147 | // 2. Used in IIS sessions like /S(dfd346)/ 148 | twttr.txt.regexen.wikipediaDisambiguation = R(/(?:\(#{validGeneralUrlPathChars}+\))/i); 149 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user 150 | twttr.txt.regexen.validUrlPathChars = R(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.\,]?#{validGeneralUrlPathChars})/i); 151 | 152 | // Valid end-of-path chracters (so /foo. does not gobble the period). 153 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 154 | twttr.txt.regexen.validUrlPathEndingChars = /[a-z0-9=#\/]/i; 155 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i; 156 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#]/i; 157 | twttr.txt.regexen.validUrl = R( 158 | '(' + // $1 total match 159 | '(#{validPrecedingChars})' + // $2 Preceeding chracter 160 | '(' + // $3 URL 161 | '((?:https?:\\/\\/|www\\.)?)' + // $4 Protocol or beginning 162 | '(#{validDomain})' + // $5 Domain(s) and optional post number 163 | '(' + // $6 URL Path 164 | '\\/#{validUrlPathChars}*' + 165 | '#{validUrlPathEndingChars}?' + 166 | ')?' + 167 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String 168 | ')' + 169 | ')' 170 | , "gi"); 171 | 172 | // Default CSS class for auto-linked URLs 173 | var DEFAULT_URL_CLASS = "tweet-url"; 174 | // Default CSS class for auto-linked lists (along with the url class) 175 | var DEFAULT_LIST_CLASS = "list-slug"; 176 | // Default CSS class for auto-linked usernames (along with the url class) 177 | var DEFAULT_USERNAME_CLASS = "username"; 178 | // Default CSS class for auto-linked hashtags (along with the url class) 179 | var DEFAULT_HASHTAG_CLASS = "hashtag"; 180 | // HTML attribute for robot nofollow behavior (default) 181 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; 182 | 183 | // Simple object cloning function for simple objects 184 | function clone(o) { 185 | var r = {}; 186 | for (var k in o) { 187 | if (o.hasOwnProperty(k)) { 188 | r[k] = o[k]; 189 | } 190 | } 191 | 192 | return r; 193 | } 194 | 195 | twttr.txt.autoLink = function(text, options) { 196 | options = clone(options || {}); 197 | return twttr.txt.autoLinkUsernamesOrLists( 198 | twttr.txt.autoLinkUrlsCustom( 199 | twttr.txt.autoLinkHashtags(text, options), 200 | options), 201 | options); 202 | }; 203 | 204 | 205 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) { 206 | options = clone(options || {}); 207 | 208 | // options = options.dup 209 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 210 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 211 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 212 | options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; 213 | options.listUrlBase = options.listUrlBase || "http://twitter.com/"; 214 | if (!options.suppressNoFollow) { 215 | var extraHtml = HTML_ATTR_NO_FOLLOW; 216 | } 217 | 218 | var newText = "", 219 | splitText = twttr.txt.splitTags(text); 220 | 221 | for (var index = 0; index < splitText.length; index++) { 222 | var chunk = splitText[index]; 223 | 224 | if (index !== 0) { 225 | newText += ((index % 2 === 0) ? ">" : "<"); 226 | } 227 | 228 | if (index % 4 !== 0) { 229 | newText += chunk; 230 | } else { 231 | newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, after) { 232 | var d = { 233 | before: before, 234 | at: at, 235 | user: twttr.txt.encode(user), 236 | slashListname: twttr.txt.encode(slashListname), 237 | after: after, 238 | extraHtml: extraHtml, 239 | chunk: twttr.txt.encode(chunk) 240 | }; 241 | for (var k in options) { 242 | if (options.hasOwnProperty(k)) { 243 | d[k] = options[k]; 244 | } 245 | } 246 | 247 | if (slashListname && !options.suppressLists) { 248 | // the link is a list 249 | var list = d.chunk = S("#{user}#{slashListname}", d); 250 | d.list = twttr.txt.encode(list.toLowerCase()); 251 | return S("#{before}#{at}#{chunk}#{after}", d); 252 | } else { 253 | if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { 254 | // Followed by something that means we don't autolink 255 | return match; 256 | } else { 257 | // this is a screen name 258 | d.chunk = twttr.txt.encode(user); 259 | d.dataScreenName = !options.suppressDataScreenName ? S("data-screen-name=\"#{chunk}\" ", d) : ""; 260 | return S("#{before}#{at}#{chunk}#{after}", d); 261 | } 262 | } 263 | }); 264 | } 265 | } 266 | 267 | return newText; 268 | }; 269 | 270 | twttr.txt.autoLinkHashtags = function(text, options) { 271 | options = clone(options || {}); 272 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 273 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 274 | options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; 275 | if (!options.suppressNoFollow) { 276 | var extraHtml = HTML_ATTR_NO_FOLLOW; 277 | } 278 | 279 | return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) { 280 | var d = { 281 | before: before, 282 | hash: twttr.txt.encode(hash), 283 | text: twttr.txt.encode(text), 284 | extraHtml: extraHtml 285 | }; 286 | 287 | for (var k in options) { 288 | if (options.hasOwnProperty(k)) { 289 | d[k] = options[k]; 290 | } 291 | } 292 | 293 | return S("#{before}#{hash}#{text}", d); 294 | }); 295 | }; 296 | 297 | 298 | twttr.txt.autoLinkUrlsCustom = function(text, options) { 299 | options = clone(options || {}); 300 | if (!options.suppressNoFollow) { 301 | options.rel = "nofollow"; 302 | } 303 | if (options.urlClass) { 304 | options["class"] = options.urlClass; 305 | delete options.urlClass; 306 | } 307 | 308 | delete options.suppressNoFollow; 309 | delete options.suppressDataScreenName; 310 | 311 | return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) { 312 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 313 | var htmlAttrs = ""; 314 | for (var k in options) { 315 | htmlAttrs += S(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); 316 | } 317 | options.htmlAttrs || ""; 318 | var fullUrl = ((!protocol || protocol.match(twttr.txt.regexen.www)) ? S("http://#{url}", {url: url}) : url); 319 | 320 | var d = { 321 | before: before, 322 | fullUrl: twttr.txt.encode(fullUrl), 323 | htmlAttrs: htmlAttrs, 324 | url: twttr.txt.encode(url) 325 | }; 326 | 327 | return S("#{before}#{url}", d); 328 | } else { 329 | return all; 330 | } 331 | }); 332 | }; 333 | 334 | twttr.txt.extractMentions = function(text) { 335 | var screenNamesOnly = [], 336 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); 337 | 338 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 339 | var screenName = screenNamesWithIndices[i].screenName; 340 | screenNamesOnly.push(screenName); 341 | } 342 | 343 | return screenNamesOnly; 344 | }; 345 | 346 | twttr.txt.extractMentionsWithIndices = function(text) { 347 | if (!text) { 348 | return []; 349 | } 350 | 351 | var possibleScreenNames = [], 352 | position = 0; 353 | 354 | text.replace(twttr.txt.regexen.extractMentions, function(match, before, screenName, after) { 355 | if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { 356 | var startPosition = text.indexOf(screenName, position) - 1; 357 | position = startPosition + screenName.length + 1; 358 | possibleScreenNames.push({ 359 | screenName: screenName, 360 | indices: [startPosition, position] 361 | }); 362 | } 363 | }); 364 | 365 | return possibleScreenNames; 366 | }; 367 | 368 | twttr.txt.extractReplies = function(text) { 369 | if (!text) { 370 | return null; 371 | } 372 | 373 | var possibleScreenName = text.match(twttr.txt.regexen.extractReply); 374 | if (!possibleScreenName) { 375 | return null; 376 | } 377 | 378 | return possibleScreenName[1]; 379 | }; 380 | 381 | twttr.txt.extractUrls = function(text) { 382 | var urlsOnly = [], 383 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); 384 | 385 | for (var i = 0; i < urlsWithIndices.length; i++) { 386 | urlsOnly.push(urlsWithIndices[i].url); 387 | } 388 | 389 | return urlsOnly; 390 | }; 391 | 392 | twttr.txt.extractUrlsWithIndices = function(text) { 393 | if (!text) { 394 | return []; 395 | } 396 | 397 | var urls = [], 398 | position = 0; 399 | 400 | text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) { 401 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 402 | var startPosition = text.indexOf(url, position), 403 | position = startPosition + url.length; 404 | 405 | urls.push({ 406 | url: ((!protocol || protocol.match(twttr.txt.regexen.www)) ? S("http://#{url}", {url: url}) : url), 407 | indices: [startPosition, position] 408 | }); 409 | } 410 | }); 411 | 412 | return urls; 413 | }; 414 | 415 | twttr.txt.extractHashtags = function(text) { 416 | var hashtagsOnly = [], 417 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); 418 | 419 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 420 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 421 | } 422 | 423 | return hashtagsOnly; 424 | }; 425 | 426 | twttr.txt.extractHashtagsWithIndices = function(text) { 427 | if (!text) { 428 | return []; 429 | } 430 | 431 | var tags = [], 432 | position = 0; 433 | 434 | text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) { 435 | var startPosition = text.indexOf(hash + hashText, position); 436 | position = startPosition + hashText.length + 1; 437 | tags.push({ 438 | hashtag: hashText, 439 | indices: [startPosition, position] 440 | }); 441 | }); 442 | 443 | return tags; 444 | }; 445 | 446 | // this essentially does text.split(/<|>/) 447 | // except that won't work in IE, where empty strings are ommitted 448 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 449 | // but "<<".split("<") => ["", "", ""] 450 | twttr.txt.splitTags = function(text) { 451 | var firstSplits = text.split("<"), 452 | secondSplits, 453 | allSplits = [], 454 | split; 455 | 456 | for (var i = 0; i < firstSplits.length; i += 1) { 457 | split = firstSplits[i]; 458 | if (!split) { 459 | allSplits.push(""); 460 | } else { 461 | secondSplits = split.split(">"); 462 | for (var j = 0; j < secondSplits.length; j += 1) { 463 | allSplits.push(secondSplits[j]); 464 | } 465 | } 466 | } 467 | 468 | return allSplits; 469 | }; 470 | 471 | twttr.txt.hitHighlight = function(text, hits, options) { 472 | var defaultHighlightTag = "em"; 473 | 474 | hits = hits || []; 475 | options = options || {}; 476 | 477 | if (hits.length === 0) { 478 | return text; 479 | } 480 | 481 | var tagName = options.tag || defaultHighlightTag, 482 | tags = ["<" + tagName + ">", ""], 483 | chunks = twttr.txt.splitTags(text), 484 | split, 485 | i, 486 | j, 487 | result = "", 488 | chunkIndex = 0, 489 | chunk = chunks[0], 490 | prevChunksLen = 0, 491 | chunkCursor = 0, 492 | startInChunk = false, 493 | chunkChars = chunk, 494 | flatHits = [], 495 | index, 496 | hit, 497 | tag, 498 | placed, 499 | hitSpot; 500 | 501 | for (i = 0; i < hits.length; i += 1) { 502 | for (j = 0; j < hits[i].length; j += 1) { 503 | flatHits.push(hits[i][j]); 504 | } 505 | } 506 | 507 | for (index = 0; index < flatHits.length; index += 1) { 508 | hit = flatHits[index]; 509 | tag = tags[index % 2]; 510 | placed = false; 511 | 512 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 513 | result += chunkChars.slice(chunkCursor); 514 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 515 | result += tag; 516 | placed = true; 517 | } 518 | 519 | if (chunks[chunkIndex + 1]) { 520 | result += "<" + chunks[chunkIndex + 1] + ">"; 521 | } 522 | 523 | prevChunksLen += chunkChars.length; 524 | chunkCursor = 0; 525 | chunkIndex += 2; 526 | chunk = chunks[chunkIndex]; 527 | chunkChars = chunk; 528 | startInChunk = false; 529 | } 530 | 531 | if (!placed && chunk != null) { 532 | hitSpot = hit - prevChunksLen; 533 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 534 | chunkCursor = hitSpot; 535 | if (index % 2 === 0) { 536 | startInChunk = true; 537 | } 538 | } 539 | } 540 | 541 | if (chunk != null) { 542 | if (chunkCursor < chunkChars.length) { 543 | result += chunkChars.slice(chunkCursor); 544 | } 545 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 546 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); 547 | } 548 | } 549 | 550 | return result; 551 | }; 552 | 553 | 554 | }()); -------------------------------------------------------------------------------- /pkg/twitter-text-1.0.3.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * twitter-text-js 1.0.3 3 | * 4 | * Copyright 2010 Twitter, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | * use this file except in compliance with the License. You may obtain a copy of 8 | * the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations under 16 | * the License. 17 | */ 18 | 19 | if (!window.twttr) { 20 | window.twttr = {}; 21 | } 22 | 23 | (function() { 24 | twttr.txt = {}; 25 | twttr.txt.regexen = {}; 26 | 27 | var HTML_ENTITIES = { 28 | '&': '&', 29 | '>': '>', 30 | '<': '<', 31 | '"': '"', 32 | "'": ' ' 33 | }; 34 | 35 | // HTML escaping 36 | twttr.txt.htmlEscape = function(text) { 37 | return text && text.replace(/[&"'><]/g, function(character) { 38 | return HTML_ENTITIES[character]; 39 | }); 40 | }; 41 | 42 | // Builds a RegExp 43 | function regexSupplant(regex, flags) { 44 | flags = flags || ""; 45 | if (typeof regex !== "string") { 46 | if (regex.global && flags.indexOf("g") < 0) { 47 | flags += "g"; 48 | } 49 | if (regex.ignoreCase && flags.indexOf("i") < 0) { 50 | flags += "i"; 51 | } 52 | if (regex.multiline && flags.indexOf("m") < 0) { 53 | flags += "m"; 54 | } 55 | 56 | regex = regex.source; 57 | } 58 | 59 | return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { 60 | var newRegex = twttr.txt.regexen[name] || ""; 61 | if (typeof newRegex !== "string") { 62 | newRegex = newRegex.source; 63 | } 64 | return newRegex; 65 | }), flags); 66 | } 67 | 68 | // simple string interpolation 69 | function stringSupplant(str, values) { 70 | return str.replace(/#\{(\w+)\}/g, function(match, name) { 71 | return values[name] || ""; 72 | }); 73 | } 74 | 75 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand 76 | // to access both the list of characters and a pattern suitible for use with String#split 77 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE 78 | var fromCode = String.fromCharCode; 79 | var UNICODE_SPACES = [ 80 | fromCode(0x0020), // White_Space # Zs SPACE 81 | fromCode(0x0085), // White_Space # Cc 82 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE 83 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK 84 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR 85 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR 86 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR 87 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE 88 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE 89 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE 90 | ]; 91 | 92 | for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] .. 93 | UNICODE_SPACES.push(String.fromCharCode(i)); 94 | } 95 | 96 | for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE 97 | UNICODE_SPACES.push(String.fromCharCode(i)); 98 | } 99 | 100 | twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]"); 101 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; 102 | twttr.txt.regexen.atSigns = /[@@]/; 103 | twttr.txt.regexen.extractMentions = regexSupplant(/(^|[^a-zA-Z0-9_])#{atSigns}([a-zA-Z0-9_]{1,20})(?=(.|$))/g); 104 | twttr.txt.regexen.extractReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); 105 | twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; 106 | 107 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") 108 | twttr.txt.regexen.latinAccentChars = regexSupplant("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277"); 109 | twttr.txt.regexen.latenAccents = regexSupplant(/[#{latinAccentChars}]+/); 110 | 111 | twttr.txt.regexen.endScreenNameMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/); 112 | 113 | // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid. 114 | twttr.txt.regexen.hashtagCharacters = regexSupplant(/[a-z0-9_#{latinAccentChars}]/i); 115 | twttr.txt.regexen.autoLinkHashtags = regexSupplant(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi); 116 | twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g; 117 | twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; 118 | 119 | // URL related hash regex collection 120 | twttr.txt.regexen.validPrecedingChars = regexSupplant(/(?:[^-\/"':!=A-Za-z0-9_@@]|^|\:)/); 121 | twttr.txt.regexen.validDomain = regexSupplant(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i); 122 | 123 | // For protocol-less URLs, we'll accept them if they end in one of a handful of likely TLDs 124 | twttr.txt.regexen.probableTld = /\.(?:com|net|org|gov|edu)$/i; 125 | 126 | twttr.txt.regexen.www = /www\./i; 127 | 128 | twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i; 129 | // Allow URL paths to contain balanced parens 130 | // 1. Used in Wikipedia URLs like /Primer_(film) 131 | // 2. Used in IIS sessions like /S(dfd346)/ 132 | twttr.txt.regexen.wikipediaDisambiguation = regexSupplant(/(?:\(#{validGeneralUrlPathChars}+\))/i); 133 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user 134 | twttr.txt.regexen.validUrlPathChars = regexSupplant(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.\,]?#{validGeneralUrlPathChars})/i); 135 | 136 | // Valid end-of-path chracters (so /foo. does not gobble the period). 137 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 138 | twttr.txt.regexen.validUrlPathEndingChars = /[a-z0-9=#\/]/i; 139 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i; 140 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#]/i; 141 | twttr.txt.regexen.validUrl = regexSupplant( 142 | '(' + // $1 total match 143 | '(#{validPrecedingChars})' + // $2 Preceeding chracter 144 | '(' + // $3 URL 145 | '((?:https?:\\/\\/|www\\.)?)' + // $4 Protocol or beginning 146 | '(#{validDomain})' + // $5 Domain(s) and optional post number 147 | '(' + // $6 URL Path 148 | '\\/#{validUrlPathChars}*' + 149 | '#{validUrlPathEndingChars}?' + 150 | ')?' + 151 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String 152 | ')' + 153 | ')' 154 | , "gi"); 155 | 156 | // Default CSS class for auto-linked URLs 157 | var DEFAULT_URL_CLASS = "tweet-url"; 158 | // Default CSS class for auto-linked lists (along with the url class) 159 | var DEFAULT_LIST_CLASS = "list-slug"; 160 | // Default CSS class for auto-linked usernames (along with the url class) 161 | var DEFAULT_USERNAME_CLASS = "username"; 162 | // Default CSS class for auto-linked hashtags (along with the url class) 163 | var DEFAULT_HASHTAG_CLASS = "hashtag"; 164 | // HTML attribute for robot nofollow behavior (default) 165 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; 166 | 167 | // Simple object cloning function for simple objects 168 | function clone(o) { 169 | var r = {}; 170 | for (var k in o) { 171 | if (o.hasOwnProperty(k)) { 172 | r[k] = o[k]; 173 | } 174 | } 175 | 176 | return r; 177 | } 178 | 179 | twttr.txt.autoLink = function(text, options) { 180 | options = clone(options || {}); 181 | return twttr.txt.autoLinkUsernamesOrLists( 182 | twttr.txt.autoLinkUrlsCustom( 183 | twttr.txt.autoLinkHashtags(text, options), 184 | options), 185 | options); 186 | }; 187 | 188 | 189 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) { 190 | options = clone(options || {}); 191 | 192 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 193 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 194 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 195 | options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; 196 | options.listUrlBase = options.listUrlBase || "http://twitter.com/"; 197 | if (!options.suppressNoFollow) { 198 | var extraHtml = HTML_ATTR_NO_FOLLOW; 199 | } 200 | 201 | var newText = "", 202 | splitText = twttr.txt.splitTags(text); 203 | 204 | for (var index = 0; index < splitText.length; index++) { 205 | var chunk = splitText[index]; 206 | 207 | if (index !== 0) { 208 | newText += ((index % 2 === 0) ? ">" : "<"); 209 | } 210 | 211 | if (index % 4 !== 0) { 212 | newText += chunk; 213 | } else { 214 | newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) { 215 | var after = chunk.slice(offset + match.length); 216 | 217 | var d = { 218 | before: before, 219 | at: at, 220 | user: twttr.txt.htmlEscape(user), 221 | slashListname: twttr.txt.htmlEscape(slashListname), 222 | extraHtml: extraHtml, 223 | chunk: twttr.txt.htmlEscape(chunk) 224 | }; 225 | for (var k in options) { 226 | if (options.hasOwnProperty(k)) { 227 | d[k] = options[k]; 228 | } 229 | } 230 | 231 | if (slashListname && !options.suppressLists) { 232 | // the link is a list 233 | var list = d.chunk = stringSupplant("#{user}#{slashListname}", d); 234 | d.list = twttr.txt.htmlEscape(list.toLowerCase()); 235 | return stringSupplant("#{before}#{at}#{chunk}", d); 236 | } else { 237 | if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { 238 | // Followed by something that means we don't autolink 239 | return match; 240 | } else { 241 | // this is a screen name 242 | d.chunk = twttr.txt.htmlEscape(user); 243 | d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : ""; 244 | return stringSupplant("#{before}#{at}#{chunk}", d); 245 | } 246 | } 247 | }); 248 | } 249 | } 250 | 251 | return newText; 252 | }; 253 | 254 | twttr.txt.autoLinkHashtags = function(text, options) { 255 | options = clone(options || {}); 256 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 257 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 258 | options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; 259 | if (!options.suppressNoFollow) { 260 | var extraHtml = HTML_ATTR_NO_FOLLOW; 261 | } 262 | 263 | return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) { 264 | var d = { 265 | before: before, 266 | hash: twttr.txt.htmlEscape(hash), 267 | text: twttr.txt.htmlEscape(text), 268 | extraHtml: extraHtml 269 | }; 270 | 271 | for (var k in options) { 272 | if (options.hasOwnProperty(k)) { 273 | d[k] = options[k]; 274 | } 275 | } 276 | 277 | return stringSupplant("#{before}#{hash}#{text}", d); 278 | }); 279 | }; 280 | 281 | 282 | twttr.txt.autoLinkUrlsCustom = function(text, options) { 283 | options = clone(options || {}); 284 | if (!options.suppressNoFollow) { 285 | options.rel = "nofollow"; 286 | } 287 | if (options.urlClass) { 288 | options["class"] = options.urlClass; 289 | delete options.urlClass; 290 | } 291 | 292 | delete options.suppressNoFollow; 293 | delete options.suppressDataScreenName; 294 | 295 | return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) { 296 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 297 | var htmlAttrs = ""; 298 | for (var k in options) { 299 | htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); 300 | } 301 | options.htmlAttrs || ""; 302 | var fullUrl = ((!protocol || protocol.match(twttr.txt.regexen.www)) ? stringSupplant("http://#{url}", {url: url}) : url); 303 | 304 | var d = { 305 | before: before, 306 | fullUrl: twttr.txt.htmlEscape(fullUrl), 307 | htmlAttrs: htmlAttrs, 308 | url: twttr.txt.htmlEscape(url) 309 | }; 310 | 311 | return stringSupplant("#{before}#{url}", d); 312 | } else { 313 | return all; 314 | } 315 | }); 316 | }; 317 | 318 | twttr.txt.extractMentions = function(text) { 319 | var screenNamesOnly = [], 320 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); 321 | 322 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 323 | var screenName = screenNamesWithIndices[i].screenName; 324 | screenNamesOnly.push(screenName); 325 | } 326 | 327 | return screenNamesOnly; 328 | }; 329 | 330 | twttr.txt.extractMentionsWithIndices = function(text) { 331 | if (!text) { 332 | return []; 333 | } 334 | 335 | var possibleScreenNames = [], 336 | position = 0; 337 | 338 | text.replace(twttr.txt.regexen.extractMentions, function(match, before, screenName, after) { 339 | if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { 340 | var startPosition = text.indexOf(screenName, position) - 1; 341 | position = startPosition + screenName.length + 1; 342 | possibleScreenNames.push({ 343 | screenName: screenName, 344 | indices: [startPosition, position] 345 | }); 346 | } 347 | }); 348 | 349 | return possibleScreenNames; 350 | }; 351 | 352 | twttr.txt.extractReplies = function(text) { 353 | if (!text) { 354 | return null; 355 | } 356 | 357 | var possibleScreenName = text.match(twttr.txt.regexen.extractReply); 358 | if (!possibleScreenName) { 359 | return null; 360 | } 361 | 362 | return possibleScreenName[1]; 363 | }; 364 | 365 | twttr.txt.extractUrls = function(text) { 366 | var urlsOnly = [], 367 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); 368 | 369 | for (var i = 0; i < urlsWithIndices.length; i++) { 370 | urlsOnly.push(urlsWithIndices[i].url); 371 | } 372 | 373 | return urlsOnly; 374 | }; 375 | 376 | twttr.txt.extractUrlsWithIndices = function(text) { 377 | if (!text) { 378 | return []; 379 | } 380 | 381 | var urls = [], 382 | position = 0; 383 | 384 | text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) { 385 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 386 | var startPosition = text.indexOf(url, position), 387 | position = startPosition + url.length; 388 | 389 | urls.push({ 390 | url: ((!protocol || protocol.match(twttr.txt.regexen.www)) ? stringSupplant("http://#{url}", {url: url}) : url), 391 | indices: [startPosition, position] 392 | }); 393 | } 394 | }); 395 | 396 | return urls; 397 | }; 398 | 399 | twttr.txt.extractHashtags = function(text) { 400 | var hashtagsOnly = [], 401 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); 402 | 403 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 404 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 405 | } 406 | 407 | return hashtagsOnly; 408 | }; 409 | 410 | twttr.txt.extractHashtagsWithIndices = function(text) { 411 | if (!text) { 412 | return []; 413 | } 414 | 415 | var tags = [], 416 | position = 0; 417 | 418 | text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) { 419 | var startPosition = text.indexOf(hash + hashText, position); 420 | position = startPosition + hashText.length + 1; 421 | tags.push({ 422 | hashtag: hashText, 423 | indices: [startPosition, position] 424 | }); 425 | }); 426 | 427 | return tags; 428 | }; 429 | 430 | // this essentially does text.split(/<|>/) 431 | // except that won't work in IE, where empty strings are ommitted 432 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 433 | // but "<<".split("<") => ["", "", ""] 434 | twttr.txt.splitTags = function(text) { 435 | var firstSplits = text.split("<"), 436 | secondSplits, 437 | allSplits = [], 438 | split; 439 | 440 | for (var i = 0; i < firstSplits.length; i += 1) { 441 | split = firstSplits[i]; 442 | if (!split) { 443 | allSplits.push(""); 444 | } else { 445 | secondSplits = split.split(">"); 446 | for (var j = 0; j < secondSplits.length; j += 1) { 447 | allSplits.push(secondSplits[j]); 448 | } 449 | } 450 | } 451 | 452 | return allSplits; 453 | }; 454 | 455 | twttr.txt.hitHighlight = function(text, hits, options) { 456 | var defaultHighlightTag = "em"; 457 | 458 | hits = hits || []; 459 | options = options || {}; 460 | 461 | if (hits.length === 0) { 462 | return text; 463 | } 464 | 465 | var tagName = options.tag || defaultHighlightTag, 466 | tags = ["<" + tagName + ">", ""], 467 | chunks = twttr.txt.splitTags(text), 468 | split, 469 | i, 470 | j, 471 | result = "", 472 | chunkIndex = 0, 473 | chunk = chunks[0], 474 | prevChunksLen = 0, 475 | chunkCursor = 0, 476 | startInChunk = false, 477 | chunkChars = chunk, 478 | flatHits = [], 479 | index, 480 | hit, 481 | tag, 482 | placed, 483 | hitSpot; 484 | 485 | for (i = 0; i < hits.length; i += 1) { 486 | for (j = 0; j < hits[i].length; j += 1) { 487 | flatHits.push(hits[i][j]); 488 | } 489 | } 490 | 491 | for (index = 0; index < flatHits.length; index += 1) { 492 | hit = flatHits[index]; 493 | tag = tags[index % 2]; 494 | placed = false; 495 | 496 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 497 | result += chunkChars.slice(chunkCursor); 498 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 499 | result += tag; 500 | placed = true; 501 | } 502 | 503 | if (chunks[chunkIndex + 1]) { 504 | result += "<" + chunks[chunkIndex + 1] + ">"; 505 | } 506 | 507 | prevChunksLen += chunkChars.length; 508 | chunkCursor = 0; 509 | chunkIndex += 2; 510 | chunk = chunks[chunkIndex]; 511 | chunkChars = chunk; 512 | startInChunk = false; 513 | } 514 | 515 | if (!placed && chunk != null) { 516 | hitSpot = hit - prevChunksLen; 517 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 518 | chunkCursor = hitSpot; 519 | if (index % 2 === 0) { 520 | startInChunk = true; 521 | } 522 | } 523 | } 524 | 525 | if (chunk != null) { 526 | if (chunkCursor < chunkChars.length) { 527 | result += chunkChars.slice(chunkCursor); 528 | } 529 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 530 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); 531 | } 532 | } 533 | 534 | return result; 535 | }; 536 | 537 | 538 | }()); -------------------------------------------------------------------------------- /pkg/twitter-text-1.0.1.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * twitter-text-js 1.0.1 3 | * 4 | * Copyright 2010 Twitter, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | * use this file except in compliance with the License. You may obtain a copy of 8 | * the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations under 16 | * the License. 17 | */ 18 | 19 | if (!window.twttr) { 20 | window.twttr = {}; 21 | } 22 | 23 | (function() { 24 | twttr.txt = {}; 25 | twttr.txt.regexen = {}; 26 | 27 | var HTML_ENTITIES = { 28 | '&': '&', 29 | '>': '>', 30 | '<': '<', 31 | '"': '"', 32 | "'": ' ' 33 | }; 34 | 35 | // HTML escaping 36 | twttr.txt.encode = function(text) { 37 | return text && text.replace(/[&"'><]/g, function(character) { 38 | return HTML_ENTITIES[character]; 39 | }); 40 | }; 41 | 42 | // Builds a RegExp 43 | function R(r, f) { 44 | f = f || ""; 45 | if (typeof r !== "string") { 46 | if (r.global && f.indexOf("g") < 0) { 47 | f += "g"; 48 | } 49 | if (r.ignoreCase && f.indexOf("i") < 0) { 50 | f += "i"; 51 | } 52 | if (r.multiline && f.indexOf("m") < 0) { 53 | f += "m"; 54 | } 55 | 56 | r = r.source; 57 | } 58 | 59 | return new RegExp(r.replace(/#\{(\w+)\}/g, function(m, name) { 60 | var regex = twttr.txt.regexen[name] || ""; 61 | if (typeof regex !== "string") { 62 | regex = regex.source; 63 | } 64 | return regex; 65 | }), f); 66 | } 67 | 68 | // simple string interpolation 69 | function S(s, d) { 70 | return s.replace(/#\{(\w+)\}/g, function(m, name) { 71 | return d[name] || ""; 72 | }); 73 | } 74 | 75 | // Join Regexes 76 | function J(a, f) { 77 | var r = ""; 78 | for (var i = 0; i < a.length; i++) { 79 | var s = a[i]; 80 | if (typeof s !== "string") { 81 | s = s.source; 82 | } 83 | r += s; 84 | } 85 | return new RegExp(r, f); 86 | } 87 | 88 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand 89 | // to access both the list of characters and a pattern suitible for use with String#split 90 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE 91 | var fromCode = String.fromCharCode; 92 | var UNICODE_SPACES = [ 93 | fromCode(0x0020), // White_Space # Zs SPACE 94 | fromCode(0x0085), // White_Space # Cc 95 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE 96 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK 97 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR 98 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR 99 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR 100 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE 101 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE 102 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE 103 | ]; 104 | 105 | for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] .. 106 | UNICODE_SPACES.push(String.fromCharCode(i)); 107 | } 108 | 109 | for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE 110 | UNICODE_SPACES.push(String.fromCharCode(i)); 111 | } 112 | 113 | twttr.txt.regexen.spaces = R("[" + UNICODE_SPACES.join("") + "]"); 114 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; 115 | twttr.txt.regexen.atSigns = /[@@]/; 116 | twttr.txt.regexen.extractMentions = R(/(^|[^a-zA-Z0-9_])#{atSigns}([a-zA-Z0-9_]{1,20})(?=(.|$))/g); 117 | twttr.txt.regexen.extractReply = R(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); 118 | twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; 119 | 120 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") 121 | var LATIN_ACCENTS = [ 122 | // (0xc0..0xd6).to_a, (0xd8..0xf6).to_a, (0xf8..0xff).to_a 123 | ];//.flatten.pack('U*').freeze 124 | twttr.txt.regexen.latinAccentChars = R("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277"); 125 | twttr.txt.regexen.latenAccents = R(/[#{latinAccentChars}]+/); 126 | 127 | twttr.txt.regexen.endScreenNameMatch = R(/^#{atSigns}|[#{latinAccentChars}]/); 128 | 129 | // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid. 130 | twttr.txt.regexen.hashtagCharacters = R(/[a-z0-9_#{latinAccentChars}]/i); 131 | twttr.txt.regexen.autoLinkHashtags = R(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi); 132 | twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g; 133 | twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; 134 | 135 | // URL related hash regex collection 136 | twttr.txt.regexen.validPrecedingChars = /(?:[^-\/"':!=A-Za-z0-9_]|^|\:)/; 137 | twttr.txt.regexen.validDomain = R(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i); 138 | 139 | // For protocol-less URLs, we'll accept them if they end in one of a handful of likely TLDs 140 | twttr.txt.regexen.probableTld = /\.(?:com|net|org|gov|edu)$/i; 141 | 142 | twttr.txt.regexen.www = /www\./i; 143 | 144 | twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i; 145 | // Allow URL paths to contain balanced parens 146 | // 1. Used in Wikipedia URLs like /Primer_(film) 147 | // 2. Used in IIS sessions like /S(dfd346)/ 148 | twttr.txt.regexen.wikipediaDisambiguation = R(/(?:\(#{validGeneralUrlPathChars}+\))/i); 149 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user 150 | twttr.txt.regexen.validUrlPathChars = R(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.\,]?#{validGeneralUrlPathChars})/i); 151 | 152 | // Valid end-of-path chracters (so /foo. does not gobble the period). 153 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 154 | twttr.txt.regexen.validUrlPathEndingChars = /[a-z0-9=#\/]/i; 155 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i; 156 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#]/i; 157 | twttr.txt.regexen.validUrl = R( 158 | '(' + // $1 total match 159 | '(#{validPrecedingChars})' + // $2 Preceeding chracter 160 | '(' + // $3 URL 161 | '((?:https?:\\/\\/|www\\.)?)' + // $4 Protocol or beginning 162 | '(#{validDomain})' + // $5 Domain(s) and optional post number 163 | '(' + // $6 URL Path 164 | '\\/#{validUrlPathChars}*' + 165 | '#{validUrlPathEndingChars}?' + 166 | ')?' + 167 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String 168 | ')' + 169 | ')' 170 | , "gi"); 171 | 172 | // Default CSS class for auto-linked URLs 173 | var DEFAULT_URL_CLASS = "tweet-url"; 174 | // Default CSS class for auto-linked lists (along with the url class) 175 | var DEFAULT_LIST_CLASS = "list-slug"; 176 | // Default CSS class for auto-linked usernames (along with the url class) 177 | var DEFAULT_USERNAME_CLASS = "username"; 178 | // Default CSS class for auto-linked hashtags (along with the url class) 179 | var DEFAULT_HASHTAG_CLASS = "hashtag"; 180 | // HTML attribute for robot nofollow behavior (default) 181 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; 182 | 183 | // Simple object cloning function for simple objects 184 | function clone(o) { 185 | var r = {}; 186 | for (var k in o) { 187 | if (o.hasOwnProperty(k)) { 188 | r[k] = o[k]; 189 | } 190 | } 191 | 192 | return r; 193 | } 194 | 195 | twttr.txt.autoLink = function(text, options) { 196 | options = clone(options || {}); 197 | return twttr.txt.autoLinkUsernamesOrLists( 198 | twttr.txt.autoLinkUrlsCustom( 199 | twttr.txt.autoLinkHashtags(text, options), 200 | options), 201 | options); 202 | }; 203 | 204 | 205 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) { 206 | options = clone(options || {}); 207 | 208 | // options = options.dup 209 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 210 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 211 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 212 | options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; 213 | options.listUrlBase = options.listUrlBase || "http://twitter.com/"; 214 | if (!options.suppressNoFollow) { 215 | var extraHtml = HTML_ATTR_NO_FOLLOW; 216 | } 217 | 218 | var newText = "", 219 | splitText = twttr.txt.splitTags(text); 220 | 221 | for (var index = 0; index < splitText.length; index++) { 222 | var chunk = splitText[index]; 223 | 224 | if (index !== 0) { 225 | newText += ((index % 2 === 0) ? ">" : "<"); 226 | } 227 | 228 | if (index % 4 !== 0) { 229 | newText += chunk; 230 | } else { 231 | newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) { 232 | var after = chunk.slice(offset + match.length); 233 | 234 | var d = { 235 | before: before, 236 | at: at, 237 | user: twttr.txt.encode(user), 238 | slashListname: twttr.txt.encode(slashListname), 239 | extraHtml: extraHtml, 240 | chunk: twttr.txt.encode(chunk) 241 | }; 242 | for (var k in options) { 243 | if (options.hasOwnProperty(k)) { 244 | d[k] = options[k]; 245 | } 246 | } 247 | 248 | if (slashListname && !options.suppressLists) { 249 | // the link is a list 250 | var list = d.chunk = S("#{user}#{slashListname}", d); 251 | d.list = twttr.txt.encode(list.toLowerCase()); 252 | return S("#{before}#{at}#{chunk}", d); 253 | } else { 254 | if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { 255 | // Followed by something that means we don't autolink 256 | return match; 257 | } else { 258 | // this is a screen name 259 | d.chunk = twttr.txt.encode(user); 260 | d.dataScreenName = !options.suppressDataScreenName ? S("data-screen-name=\"#{chunk}\" ", d) : ""; 261 | return S("#{before}#{at}#{chunk}", d); 262 | } 263 | } 264 | }); 265 | } 266 | } 267 | 268 | return newText; 269 | }; 270 | 271 | twttr.txt.autoLinkHashtags = function(text, options) { 272 | options = clone(options || {}); 273 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 274 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 275 | options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; 276 | if (!options.suppressNoFollow) { 277 | var extraHtml = HTML_ATTR_NO_FOLLOW; 278 | } 279 | 280 | return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) { 281 | var d = { 282 | before: before, 283 | hash: twttr.txt.encode(hash), 284 | text: twttr.txt.encode(text), 285 | extraHtml: extraHtml 286 | }; 287 | 288 | for (var k in options) { 289 | if (options.hasOwnProperty(k)) { 290 | d[k] = options[k]; 291 | } 292 | } 293 | 294 | return S("#{before}#{hash}#{text}", d); 295 | }); 296 | }; 297 | 298 | 299 | twttr.txt.autoLinkUrlsCustom = function(text, options) { 300 | options = clone(options || {}); 301 | if (!options.suppressNoFollow) { 302 | options.rel = "nofollow"; 303 | } 304 | if (options.urlClass) { 305 | options["class"] = options.urlClass; 306 | delete options.urlClass; 307 | } 308 | 309 | delete options.suppressNoFollow; 310 | delete options.suppressDataScreenName; 311 | 312 | return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) { 313 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 314 | var htmlAttrs = ""; 315 | for (var k in options) { 316 | htmlAttrs += S(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); 317 | } 318 | options.htmlAttrs || ""; 319 | var fullUrl = ((!protocol || protocol.match(twttr.txt.regexen.www)) ? S("http://#{url}", {url: url}) : url); 320 | 321 | var d = { 322 | before: before, 323 | fullUrl: twttr.txt.encode(fullUrl), 324 | htmlAttrs: htmlAttrs, 325 | url: twttr.txt.encode(url) 326 | }; 327 | 328 | return S("#{before}#{url}", d); 329 | } else { 330 | return all; 331 | } 332 | }); 333 | }; 334 | 335 | twttr.txt.extractMentions = function(text) { 336 | var screenNamesOnly = [], 337 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); 338 | 339 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 340 | var screenName = screenNamesWithIndices[i].screenName; 341 | screenNamesOnly.push(screenName); 342 | } 343 | 344 | return screenNamesOnly; 345 | }; 346 | 347 | twttr.txt.extractMentionsWithIndices = function(text) { 348 | if (!text) { 349 | return []; 350 | } 351 | 352 | var possibleScreenNames = [], 353 | position = 0; 354 | 355 | text.replace(twttr.txt.regexen.extractMentions, function(match, before, screenName, after) { 356 | if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { 357 | var startPosition = text.indexOf(screenName, position) - 1; 358 | position = startPosition + screenName.length + 1; 359 | possibleScreenNames.push({ 360 | screenName: screenName, 361 | indices: [startPosition, position] 362 | }); 363 | } 364 | }); 365 | 366 | return possibleScreenNames; 367 | }; 368 | 369 | twttr.txt.extractReplies = function(text) { 370 | if (!text) { 371 | return null; 372 | } 373 | 374 | var possibleScreenName = text.match(twttr.txt.regexen.extractReply); 375 | if (!possibleScreenName) { 376 | return null; 377 | } 378 | 379 | return possibleScreenName[1]; 380 | }; 381 | 382 | twttr.txt.extractUrls = function(text) { 383 | var urlsOnly = [], 384 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); 385 | 386 | for (var i = 0; i < urlsWithIndices.length; i++) { 387 | urlsOnly.push(urlsWithIndices[i].url); 388 | } 389 | 390 | return urlsOnly; 391 | }; 392 | 393 | twttr.txt.extractUrlsWithIndices = function(text) { 394 | if (!text) { 395 | return []; 396 | } 397 | 398 | var urls = [], 399 | position = 0; 400 | 401 | text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) { 402 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 403 | var startPosition = text.indexOf(url, position), 404 | position = startPosition + url.length; 405 | 406 | urls.push({ 407 | url: ((!protocol || protocol.match(twttr.txt.regexen.www)) ? S("http://#{url}", {url: url}) : url), 408 | indices: [startPosition, position] 409 | }); 410 | } 411 | }); 412 | 413 | return urls; 414 | }; 415 | 416 | twttr.txt.extractHashtags = function(text) { 417 | var hashtagsOnly = [], 418 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); 419 | 420 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 421 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 422 | } 423 | 424 | return hashtagsOnly; 425 | }; 426 | 427 | twttr.txt.extractHashtagsWithIndices = function(text) { 428 | if (!text) { 429 | return []; 430 | } 431 | 432 | var tags = [], 433 | position = 0; 434 | 435 | text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) { 436 | var startPosition = text.indexOf(hash + hashText, position); 437 | position = startPosition + hashText.length + 1; 438 | tags.push({ 439 | hashtag: hashText, 440 | indices: [startPosition, position] 441 | }); 442 | }); 443 | 444 | return tags; 445 | }; 446 | 447 | // this essentially does text.split(/<|>/) 448 | // except that won't work in IE, where empty strings are ommitted 449 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 450 | // but "<<".split("<") => ["", "", ""] 451 | twttr.txt.splitTags = function(text) { 452 | var firstSplits = text.split("<"), 453 | secondSplits, 454 | allSplits = [], 455 | split; 456 | 457 | for (var i = 0; i < firstSplits.length; i += 1) { 458 | split = firstSplits[i]; 459 | if (!split) { 460 | allSplits.push(""); 461 | } else { 462 | secondSplits = split.split(">"); 463 | for (var j = 0; j < secondSplits.length; j += 1) { 464 | allSplits.push(secondSplits[j]); 465 | } 466 | } 467 | } 468 | 469 | return allSplits; 470 | }; 471 | 472 | twttr.txt.hitHighlight = function(text, hits, options) { 473 | var defaultHighlightTag = "em"; 474 | 475 | hits = hits || []; 476 | options = options || {}; 477 | 478 | if (hits.length === 0) { 479 | return text; 480 | } 481 | 482 | var tagName = options.tag || defaultHighlightTag, 483 | tags = ["<" + tagName + ">", ""], 484 | chunks = twttr.txt.splitTags(text), 485 | split, 486 | i, 487 | j, 488 | result = "", 489 | chunkIndex = 0, 490 | chunk = chunks[0], 491 | prevChunksLen = 0, 492 | chunkCursor = 0, 493 | startInChunk = false, 494 | chunkChars = chunk, 495 | flatHits = [], 496 | index, 497 | hit, 498 | tag, 499 | placed, 500 | hitSpot; 501 | 502 | for (i = 0; i < hits.length; i += 1) { 503 | for (j = 0; j < hits[i].length; j += 1) { 504 | flatHits.push(hits[i][j]); 505 | } 506 | } 507 | 508 | for (index = 0; index < flatHits.length; index += 1) { 509 | hit = flatHits[index]; 510 | tag = tags[index % 2]; 511 | placed = false; 512 | 513 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 514 | result += chunkChars.slice(chunkCursor); 515 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 516 | result += tag; 517 | placed = true; 518 | } 519 | 520 | if (chunks[chunkIndex + 1]) { 521 | result += "<" + chunks[chunkIndex + 1] + ">"; 522 | } 523 | 524 | prevChunksLen += chunkChars.length; 525 | chunkCursor = 0; 526 | chunkIndex += 2; 527 | chunk = chunks[chunkIndex]; 528 | chunkChars = chunk; 529 | startInChunk = false; 530 | } 531 | 532 | if (!placed && chunk != null) { 533 | hitSpot = hit - prevChunksLen; 534 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 535 | chunkCursor = hitSpot; 536 | if (index % 2 === 0) { 537 | startInChunk = true; 538 | } 539 | } 540 | } 541 | 542 | if (chunk != null) { 543 | if (chunkCursor < chunkChars.length) { 544 | result += chunkChars.slice(chunkCursor); 545 | } 546 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 547 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); 548 | } 549 | } 550 | 551 | return result; 552 | }; 553 | 554 | 555 | }()); -------------------------------------------------------------------------------- /pkg/twitter-text-1.0.4.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * twitter-text-js 1.0.4 3 | * 4 | * Copyright 2010 Twitter, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | * use this file except in compliance with the License. You may obtain a copy of 8 | * the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations under 16 | * the License. 17 | */ 18 | 19 | if (!window.twttr) { 20 | window.twttr = {}; 21 | } 22 | 23 | (function() { 24 | twttr.txt = {}; 25 | twttr.txt.regexen = {}; 26 | 27 | var HTML_ENTITIES = { 28 | '&': '&', 29 | '>': '>', 30 | '<': '<', 31 | '"': '"', 32 | "'": ' ' 33 | }; 34 | 35 | // HTML escaping 36 | twttr.txt.htmlEscape = function(text) { 37 | return text && text.replace(/[&"'><]/g, function(character) { 38 | return HTML_ENTITIES[character]; 39 | }); 40 | }; 41 | 42 | // Builds a RegExp 43 | function regexSupplant(regex, flags) { 44 | flags = flags || ""; 45 | if (typeof regex !== "string") { 46 | if (regex.global && flags.indexOf("g") < 0) { 47 | flags += "g"; 48 | } 49 | if (regex.ignoreCase && flags.indexOf("i") < 0) { 50 | flags += "i"; 51 | } 52 | if (regex.multiline && flags.indexOf("m") < 0) { 53 | flags += "m"; 54 | } 55 | 56 | regex = regex.source; 57 | } 58 | 59 | return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { 60 | var newRegex = twttr.txt.regexen[name] || ""; 61 | if (typeof newRegex !== "string") { 62 | newRegex = newRegex.source; 63 | } 64 | return newRegex; 65 | }), flags); 66 | } 67 | 68 | // simple string interpolation 69 | function stringSupplant(str, values) { 70 | return str.replace(/#\{(\w+)\}/g, function(match, name) { 71 | return values[name] || ""; 72 | }); 73 | } 74 | 75 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand 76 | // to access both the list of characters and a pattern suitible for use with String#split 77 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE 78 | var fromCode = String.fromCharCode; 79 | var UNICODE_SPACES = [ 80 | fromCode(0x0020), // White_Space # Zs SPACE 81 | fromCode(0x0085), // White_Space # Cc 82 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE 83 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK 84 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR 85 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR 86 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR 87 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE 88 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE 89 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE 90 | ]; 91 | 92 | for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] .. 93 | UNICODE_SPACES.push(String.fromCharCode(i)); 94 | } 95 | 96 | for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE 97 | UNICODE_SPACES.push(String.fromCharCode(i)); 98 | } 99 | 100 | twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]"); 101 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; 102 | twttr.txt.regexen.atSigns = /[@@]/; 103 | twttr.txt.regexen.extractMentions = regexSupplant(/(^|[^a-zA-Z0-9_])#{atSigns}([a-zA-Z0-9_]{1,20})(?=(.|$))/g); 104 | twttr.txt.regexen.extractReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); 105 | twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; 106 | 107 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") 108 | twttr.txt.regexen.latinAccentChars = regexSupplant("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277"); 109 | twttr.txt.regexen.latenAccents = regexSupplant(/[#{latinAccentChars}]+/); 110 | 111 | twttr.txt.regexen.endScreenNameMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/); 112 | 113 | // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid. 114 | twttr.txt.regexen.hashtagCharacters = regexSupplant(/[a-z0-9_#{latinAccentChars}]/i); 115 | twttr.txt.regexen.autoLinkHashtags = regexSupplant(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi); 116 | twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g; 117 | twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; 118 | 119 | // URL related hash regex collection 120 | twttr.txt.regexen.validPrecedingChars = regexSupplant(/(?:[^-\/"':!=A-Za-z0-9_@@]|^|\:)/); 121 | twttr.txt.regexen.validDomain = regexSupplant(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i); 122 | 123 | // For protocol-less URLs, we'll accept them if they end in one of a handful of likely TLDs 124 | twttr.txt.regexen.probableTld = /\.(?:com|net|org|gov|edu)$/i; 125 | 126 | twttr.txt.regexen.www = /www\./i; 127 | 128 | twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i; 129 | // Allow URL paths to contain balanced parens 130 | // 1. Used in Wikipedia URLs like /Primer_(film) 131 | // 2. Used in IIS sessions like /S(dfd346)/ 132 | twttr.txt.regexen.wikipediaDisambiguation = regexSupplant(/(?:\(#{validGeneralUrlPathChars}+\))/i); 133 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user 134 | twttr.txt.regexen.validUrlPathChars = regexSupplant(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.\,]?#{validGeneralUrlPathChars})/i); 135 | 136 | // Valid end-of-path chracters (so /foo. does not gobble the period). 137 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 138 | twttr.txt.regexen.validUrlPathEndingChars = /[a-z0-9=#\/]/i; 139 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i; 140 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#]/i; 141 | twttr.txt.regexen.validUrl = regexSupplant( 142 | '(' + // $1 total match 143 | '(#{validPrecedingChars})' + // $2 Preceeding chracter 144 | '(' + // $3 URL 145 | '((?:https?:\\/\\/|www\\.)?)' + // $4 Protocol or beginning 146 | '(#{validDomain})' + // $5 Domain(s) and optional post number 147 | '(' + // $6 URL Path 148 | '\\/#{validUrlPathChars}*' + 149 | '#{validUrlPathEndingChars}?' + 150 | ')?' + 151 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String 152 | ')' + 153 | ')' 154 | , "gi"); 155 | 156 | // Default CSS class for auto-linked URLs 157 | var DEFAULT_URL_CLASS = "tweet-url"; 158 | // Default CSS class for auto-linked lists (along with the url class) 159 | var DEFAULT_LIST_CLASS = "list-slug"; 160 | // Default CSS class for auto-linked usernames (along with the url class) 161 | var DEFAULT_USERNAME_CLASS = "username"; 162 | // Default CSS class for auto-linked hashtags (along with the url class) 163 | var DEFAULT_HASHTAG_CLASS = "hashtag"; 164 | // HTML attribute for robot nofollow behavior (default) 165 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; 166 | 167 | // Simple object cloning function for simple objects 168 | function clone(o) { 169 | var r = {}; 170 | for (var k in o) { 171 | if (o.hasOwnProperty(k)) { 172 | r[k] = o[k]; 173 | } 174 | } 175 | 176 | return r; 177 | } 178 | 179 | twttr.txt.autoLink = function(text, options) { 180 | options = clone(options || {}); 181 | return twttr.txt.autoLinkUsernamesOrLists( 182 | twttr.txt.autoLinkUrlsCustom( 183 | twttr.txt.autoLinkHashtags(text, options), 184 | options), 185 | options); 186 | }; 187 | 188 | 189 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) { 190 | options = clone(options || {}); 191 | 192 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 193 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 194 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 195 | options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; 196 | options.listUrlBase = options.listUrlBase || "http://twitter.com/"; 197 | if (!options.suppressNoFollow) { 198 | var extraHtml = HTML_ATTR_NO_FOLLOW; 199 | } 200 | 201 | var newText = "", 202 | splitText = twttr.txt.splitTags(text); 203 | 204 | for (var index = 0; index < splitText.length; index++) { 205 | var chunk = splitText[index]; 206 | 207 | if (index !== 0) { 208 | newText += ((index % 2 === 0) ? ">" : "<"); 209 | } 210 | 211 | if (index % 4 !== 0) { 212 | newText += chunk; 213 | } else { 214 | newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) { 215 | var after = chunk.slice(offset + match.length); 216 | 217 | var d = { 218 | before: before, 219 | at: at, 220 | user: twttr.txt.htmlEscape(user), 221 | slashListname: twttr.txt.htmlEscape(slashListname), 222 | extraHtml: extraHtml, 223 | chunk: twttr.txt.htmlEscape(chunk) 224 | }; 225 | for (var k in options) { 226 | if (options.hasOwnProperty(k)) { 227 | d[k] = options[k]; 228 | } 229 | } 230 | 231 | if (slashListname && !options.suppressLists) { 232 | // the link is a list 233 | var list = d.chunk = stringSupplant("#{user}#{slashListname}", d); 234 | d.list = twttr.txt.htmlEscape(list.toLowerCase()); 235 | return stringSupplant("#{before}#{at}#{chunk}", d); 236 | } else { 237 | if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { 238 | // Followed by something that means we don't autolink 239 | return match; 240 | } else { 241 | // this is a screen name 242 | d.chunk = twttr.txt.htmlEscape(user); 243 | d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : ""; 244 | return stringSupplant("#{before}#{at}#{chunk}", d); 245 | } 246 | } 247 | }); 248 | } 249 | } 250 | 251 | return newText; 252 | }; 253 | 254 | twttr.txt.autoLinkHashtags = function(text, options) { 255 | options = clone(options || {}); 256 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 257 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 258 | options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; 259 | if (!options.suppressNoFollow) { 260 | var extraHtml = HTML_ATTR_NO_FOLLOW; 261 | } 262 | 263 | return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) { 264 | var d = { 265 | before: before, 266 | hash: twttr.txt.htmlEscape(hash), 267 | text: twttr.txt.htmlEscape(text), 268 | extraHtml: extraHtml 269 | }; 270 | 271 | for (var k in options) { 272 | if (options.hasOwnProperty(k)) { 273 | d[k] = options[k]; 274 | } 275 | } 276 | 277 | return stringSupplant("#{before}#{hash}#{text}", d); 278 | }); 279 | }; 280 | 281 | 282 | twttr.txt.autoLinkUrlsCustom = function(text, options) { 283 | options = clone(options || {}); 284 | if (!options.suppressNoFollow) { 285 | options.rel = "nofollow"; 286 | } 287 | if (options.urlClass) { 288 | options["class"] = options.urlClass; 289 | delete options.urlClass; 290 | } 291 | 292 | delete options.suppressNoFollow; 293 | delete options.suppressDataScreenName; 294 | 295 | return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) { 296 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 297 | var htmlAttrs = ""; 298 | for (var k in options) { 299 | htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); 300 | } 301 | options.htmlAttrs || ""; 302 | var fullUrl = ((!protocol || protocol.match(twttr.txt.regexen.www)) ? stringSupplant("http://#{url}", {url: url}) : url); 303 | 304 | var d = { 305 | before: before, 306 | fullUrl: twttr.txt.htmlEscape(fullUrl), 307 | htmlAttrs: htmlAttrs, 308 | url: twttr.txt.htmlEscape(url) 309 | }; 310 | 311 | return stringSupplant("#{before}#{url}", d); 312 | } else { 313 | return all; 314 | } 315 | }); 316 | }; 317 | 318 | twttr.txt.extractMentions = function(text) { 319 | var screenNamesOnly = [], 320 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); 321 | 322 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 323 | var screenName = screenNamesWithIndices[i].screenName; 324 | screenNamesOnly.push(screenName); 325 | } 326 | 327 | return screenNamesOnly; 328 | }; 329 | 330 | twttr.txt.extractMentionsWithIndices = function(text) { 331 | if (!text) { 332 | return []; 333 | } 334 | 335 | var possibleScreenNames = [], 336 | position = 0; 337 | 338 | text.replace(twttr.txt.regexen.extractMentions, function(match, before, screenName, after) { 339 | if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { 340 | var startPosition = text.indexOf(screenName, position) - 1; 341 | position = startPosition + screenName.length + 1; 342 | possibleScreenNames.push({ 343 | screenName: screenName, 344 | indices: [startPosition, position] 345 | }); 346 | } 347 | }); 348 | 349 | return possibleScreenNames; 350 | }; 351 | 352 | twttr.txt.extractReplies = function(text) { 353 | if (!text) { 354 | return null; 355 | } 356 | 357 | var possibleScreenName = text.match(twttr.txt.regexen.extractReply); 358 | if (!possibleScreenName) { 359 | return null; 360 | } 361 | 362 | return possibleScreenName[1]; 363 | }; 364 | 365 | twttr.txt.extractUrls = function(text) { 366 | var urlsOnly = [], 367 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); 368 | 369 | for (var i = 0; i < urlsWithIndices.length; i++) { 370 | urlsOnly.push(urlsWithIndices[i].url); 371 | } 372 | 373 | return urlsOnly; 374 | }; 375 | 376 | twttr.txt.extractUrlsWithIndices = function(text) { 377 | if (!text) { 378 | return []; 379 | } 380 | 381 | var urls = [], 382 | position = 0; 383 | 384 | text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) { 385 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 386 | var startPosition = text.indexOf(url, position), 387 | position = startPosition + url.length; 388 | 389 | urls.push({ 390 | url: ((!protocol || protocol.match(twttr.txt.regexen.www)) ? stringSupplant("http://#{url}", {url: url}) : url), 391 | indices: [startPosition, position] 392 | }); 393 | } 394 | }); 395 | 396 | return urls; 397 | }; 398 | 399 | twttr.txt.extractHashtags = function(text) { 400 | var hashtagsOnly = [], 401 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); 402 | 403 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 404 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 405 | } 406 | 407 | return hashtagsOnly; 408 | }; 409 | 410 | twttr.txt.extractHashtagsWithIndices = function(text) { 411 | if (!text) { 412 | return []; 413 | } 414 | 415 | var tags = [], 416 | position = 0; 417 | 418 | text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) { 419 | var startPosition = text.indexOf(hash + hashText, position); 420 | position = startPosition + hashText.length + 1; 421 | tags.push({ 422 | hashtag: hashText, 423 | indices: [startPosition, position] 424 | }); 425 | }); 426 | 427 | return tags; 428 | }; 429 | 430 | // this essentially does text.split(/<|>/) 431 | // except that won't work in IE, where empty strings are ommitted 432 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 433 | // but "<<".split("<") => ["", "", ""] 434 | twttr.txt.splitTags = function(text) { 435 | var firstSplits = text.split("<"), 436 | secondSplits, 437 | allSplits = [], 438 | split; 439 | 440 | for (var i = 0; i < firstSplits.length; i += 1) { 441 | split = firstSplits[i]; 442 | if (!split) { 443 | allSplits.push(""); 444 | } else { 445 | secondSplits = split.split(">"); 446 | for (var j = 0; j < secondSplits.length; j += 1) { 447 | allSplits.push(secondSplits[j]); 448 | } 449 | } 450 | } 451 | 452 | return allSplits; 453 | }; 454 | 455 | twttr.txt.hitHighlight = function(text, hits, options) { 456 | var defaultHighlightTag = "em"; 457 | 458 | hits = hits || []; 459 | options = options || {}; 460 | 461 | if (hits.length === 0) { 462 | return text; 463 | } 464 | 465 | var tagName = options.tag || defaultHighlightTag, 466 | tags = ["<" + tagName + ">", ""], 467 | chunks = twttr.txt.splitTags(text), 468 | split, 469 | i, 470 | j, 471 | result = "", 472 | chunkIndex = 0, 473 | chunk = chunks[0], 474 | prevChunksLen = 0, 475 | chunkCursor = 0, 476 | startInChunk = false, 477 | chunkChars = chunk, 478 | flatHits = [], 479 | index, 480 | hit, 481 | tag, 482 | placed, 483 | hitSpot; 484 | 485 | for (i = 0; i < hits.length; i += 1) { 486 | for (j = 0; j < hits[i].length; j += 1) { 487 | flatHits.push(hits[i][j]); 488 | } 489 | } 490 | 491 | for (index = 0; index < flatHits.length; index += 1) { 492 | hit = flatHits[index]; 493 | tag = tags[index % 2]; 494 | placed = false; 495 | 496 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 497 | result += chunkChars.slice(chunkCursor); 498 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 499 | result += tag; 500 | placed = true; 501 | } 502 | 503 | if (chunks[chunkIndex + 1]) { 504 | result += "<" + chunks[chunkIndex + 1] + ">"; 505 | } 506 | 507 | prevChunksLen += chunkChars.length; 508 | chunkCursor = 0; 509 | chunkIndex += 2; 510 | chunk = chunks[chunkIndex]; 511 | chunkChars = chunk; 512 | startInChunk = false; 513 | } 514 | 515 | if (!placed && chunk != null) { 516 | hitSpot = hit - prevChunksLen; 517 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 518 | chunkCursor = hitSpot; 519 | if (index % 2 === 0) { 520 | startInChunk = true; 521 | } else { 522 | startInChunk = false; 523 | } 524 | } 525 | } 526 | 527 | if (chunk != null) { 528 | if (chunkCursor < chunkChars.length) { 529 | result += chunkChars.slice(chunkCursor); 530 | } 531 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 532 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); 533 | } 534 | } 535 | 536 | return result; 537 | }; 538 | 539 | 540 | }()); -------------------------------------------------------------------------------- /pkg/twitter-text-1.0.2.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * twitter-text-js 1.0.2 3 | * 4 | * Copyright 2010 Twitter, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | * use this file except in compliance with the License. You may obtain a copy of 8 | * the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations under 16 | * the License. 17 | */ 18 | 19 | if (!window.twttr) { 20 | window.twttr = {}; 21 | } 22 | 23 | (function() { 24 | twttr.txt = {}; 25 | twttr.txt.regexen = {}; 26 | 27 | var HTML_ENTITIES = { 28 | '&': '&', 29 | '>': '>', 30 | '<': '<', 31 | '"': '"', 32 | "'": ' ' 33 | }; 34 | 35 | // HTML escaping 36 | twttr.txt.htmlEscape = function(text) { 37 | return text && text.replace(/[&"'><]/g, function(character) { 38 | return HTML_ENTITIES[character]; 39 | }); 40 | }; 41 | 42 | // Builds a RegExp 43 | function regexSupplant(regex, flags) { 44 | flags = flags || ""; 45 | if (typeof regex !== "string") { 46 | if (regex.global && flags.indexOf("g") < 0) { 47 | flags += "g"; 48 | } 49 | if (regex.ignoreCase && flags.indexOf("i") < 0) { 50 | flags += "i"; 51 | } 52 | if (regex.multiline && flags.indexOf("m") < 0) { 53 | flags += "m"; 54 | } 55 | 56 | regex = regex.source; 57 | } 58 | 59 | return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { 60 | var newRegex = twttr.txt.regexen[name] || ""; 61 | if (typeof newRegex !== "string") { 62 | newRegex = newRegex.source; 63 | } 64 | return newRegex; 65 | }), flags); 66 | } 67 | 68 | // simple string interpolation 69 | function stringSupplant(str, values) { 70 | return str.replace(/#\{(\w+)\}/g, function(match, name) { 71 | return values[name] || ""; 72 | }); 73 | } 74 | 75 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand 76 | // to access both the list of characters and a pattern suitible for use with String#split 77 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE 78 | var fromCode = String.fromCharCode; 79 | var UNICODE_SPACES = [ 80 | fromCode(0x0020), // White_Space # Zs SPACE 81 | fromCode(0x0085), // White_Space # Cc 82 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE 83 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK 84 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR 85 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR 86 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR 87 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE 88 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE 89 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE 90 | ]; 91 | 92 | for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] .. 93 | UNICODE_SPACES.push(String.fromCharCode(i)); 94 | } 95 | 96 | for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE 97 | UNICODE_SPACES.push(String.fromCharCode(i)); 98 | } 99 | 100 | twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]"); 101 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; 102 | twttr.txt.regexen.atSigns = /[@@]/; 103 | twttr.txt.regexen.extractMentions = regexSupplant(/(^|[^a-zA-Z0-9_])#{atSigns}([a-zA-Z0-9_]{1,20})(?=(.|$))/g); 104 | twttr.txt.regexen.extractReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); 105 | twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; 106 | 107 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") 108 | var LATIN_ACCENTS = [ 109 | // (0xc0..0xd6).to_a, (0xd8..0xf6).to_a, (0xf8..0xff).to_a 110 | ];//.flatten.pack('U*').freeze 111 | twttr.txt.regexen.latinAccentChars = regexSupplant("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277"); 112 | twttr.txt.regexen.latenAccents = regexSupplant(/[#{latinAccentChars}]+/); 113 | 114 | twttr.txt.regexen.endScreenNameMatch = regexSupplant(/^#{atSigns}|[#{latinAccentChars}]/); 115 | 116 | // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid. 117 | twttr.txt.regexen.hashtagCharacters = regexSupplant(/[a-z0-9_#{latinAccentChars}]/i); 118 | twttr.txt.regexen.autoLinkHashtags = regexSupplant(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi); 119 | twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g; 120 | twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; 121 | 122 | // URL related hash regex collection 123 | twttr.txt.regexen.validPrecedingChars = /(?:[^-\/"':!=A-Za-z0-9_]|^|\:)/; 124 | twttr.txt.regexen.validDomain = regexSupplant(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i); 125 | 126 | // For protocol-less URLs, we'll accept them if they end in one of a handful of likely TLDs 127 | twttr.txt.regexen.probableTld = /\.(?:com|net|org|gov|edu)$/i; 128 | 129 | twttr.txt.regexen.www = /www\./i; 130 | 131 | twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i; 132 | // Allow URL paths to contain balanced parens 133 | // 1. Used in Wikipedia URLs like /Primer_(film) 134 | // 2. Used in IIS sessions like /S(dfd346)/ 135 | twttr.txt.regexen.wikipediaDisambiguation = regexSupplant(/(?:\(#{validGeneralUrlPathChars}+\))/i); 136 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user 137 | twttr.txt.regexen.validUrlPathChars = regexSupplant(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.\,]?#{validGeneralUrlPathChars})/i); 138 | 139 | // Valid end-of-path chracters (so /foo. does not gobble the period). 140 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 141 | twttr.txt.regexen.validUrlPathEndingChars = /[a-z0-9=#\/]/i; 142 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i; 143 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#]/i; 144 | twttr.txt.regexen.validUrl = regexSupplant( 145 | '(' + // $1 total match 146 | '(#{validPrecedingChars})' + // $2 Preceeding chracter 147 | '(' + // $3 URL 148 | '((?:https?:\\/\\/|www\\.)?)' + // $4 Protocol or beginning 149 | '(#{validDomain})' + // $5 Domain(s) and optional post number 150 | '(' + // $6 URL Path 151 | '\\/#{validUrlPathChars}*' + 152 | '#{validUrlPathEndingChars}?' + 153 | ')?' + 154 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String 155 | ')' + 156 | ')' 157 | , "gi"); 158 | 159 | // Default CSS class for auto-linked URLs 160 | var DEFAULT_URL_CLASS = "tweet-url"; 161 | // Default CSS class for auto-linked lists (along with the url class) 162 | var DEFAULT_LIST_CLASS = "list-slug"; 163 | // Default CSS class for auto-linked usernames (along with the url class) 164 | var DEFAULT_USERNAME_CLASS = "username"; 165 | // Default CSS class for auto-linked hashtags (along with the url class) 166 | var DEFAULT_HASHTAG_CLASS = "hashtag"; 167 | // HTML attribute for robot nofollow behavior (default) 168 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; 169 | 170 | // Simple object cloning function for simple objects 171 | function clone(o) { 172 | var r = {}; 173 | for (var k in o) { 174 | if (o.hasOwnProperty(k)) { 175 | r[k] = o[k]; 176 | } 177 | } 178 | 179 | return r; 180 | } 181 | 182 | twttr.txt.autoLink = function(text, options) { 183 | options = clone(options || {}); 184 | return twttr.txt.autoLinkUsernamesOrLists( 185 | twttr.txt.autoLinkUrlsCustom( 186 | twttr.txt.autoLinkHashtags(text, options), 187 | options), 188 | options); 189 | }; 190 | 191 | 192 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) { 193 | options = clone(options || {}); 194 | 195 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 196 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 197 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 198 | options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; 199 | options.listUrlBase = options.listUrlBase || "http://twitter.com/"; 200 | if (!options.suppressNoFollow) { 201 | var extraHtml = HTML_ATTR_NO_FOLLOW; 202 | } 203 | 204 | var newText = "", 205 | splitText = twttr.txt.splitTags(text); 206 | 207 | for (var index = 0; index < splitText.length; index++) { 208 | var chunk = splitText[index]; 209 | 210 | if (index !== 0) { 211 | newText += ((index % 2 === 0) ? ">" : "<"); 212 | } 213 | 214 | if (index % 4 !== 0) { 215 | newText += chunk; 216 | } else { 217 | newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) { 218 | var after = chunk.slice(offset + match.length); 219 | 220 | var d = { 221 | before: before, 222 | at: at, 223 | user: twttr.txt.htmlEscape(user), 224 | slashListname: twttr.txt.htmlEscape(slashListname), 225 | extraHtml: extraHtml, 226 | chunk: twttr.txt.htmlEscape(chunk) 227 | }; 228 | for (var k in options) { 229 | if (options.hasOwnProperty(k)) { 230 | d[k] = options[k]; 231 | } 232 | } 233 | 234 | if (slashListname && !options.suppressLists) { 235 | // the link is a list 236 | var list = d.chunk = stringSupplant("#{user}#{slashListname}", d); 237 | d.list = twttr.txt.htmlEscape(list.toLowerCase()); 238 | return stringSupplant("#{before}#{at}#{chunk}", d); 239 | } else { 240 | if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { 241 | // Followed by something that means we don't autolink 242 | return match; 243 | } else { 244 | // this is a screen name 245 | d.chunk = twttr.txt.htmlEscape(user); 246 | d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : ""; 247 | return stringSupplant("#{before}#{at}#{chunk}", d); 248 | } 249 | } 250 | }); 251 | } 252 | } 253 | 254 | return newText; 255 | }; 256 | 257 | twttr.txt.autoLinkHashtags = function(text, options) { 258 | options = clone(options || {}); 259 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 260 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 261 | options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; 262 | if (!options.suppressNoFollow) { 263 | var extraHtml = HTML_ATTR_NO_FOLLOW; 264 | } 265 | 266 | return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) { 267 | var d = { 268 | before: before, 269 | hash: twttr.txt.htmlEscape(hash), 270 | text: twttr.txt.htmlEscape(text), 271 | extraHtml: extraHtml 272 | }; 273 | 274 | for (var k in options) { 275 | if (options.hasOwnProperty(k)) { 276 | d[k] = options[k]; 277 | } 278 | } 279 | 280 | return stringSupplant("#{before}#{hash}#{text}", d); 281 | }); 282 | }; 283 | 284 | 285 | twttr.txt.autoLinkUrlsCustom = function(text, options) { 286 | options = clone(options || {}); 287 | if (!options.suppressNoFollow) { 288 | options.rel = "nofollow"; 289 | } 290 | if (options.urlClass) { 291 | options["class"] = options.urlClass; 292 | delete options.urlClass; 293 | } 294 | 295 | delete options.suppressNoFollow; 296 | delete options.suppressDataScreenName; 297 | 298 | return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) { 299 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 300 | var htmlAttrs = ""; 301 | for (var k in options) { 302 | htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); 303 | } 304 | options.htmlAttrs || ""; 305 | var fullUrl = ((!protocol || protocol.match(twttr.txt.regexen.www)) ? stringSupplant("http://#{url}", {url: url}) : url); 306 | 307 | var d = { 308 | before: before, 309 | fullUrl: twttr.txt.htmlEscape(fullUrl), 310 | htmlAttrs: htmlAttrs, 311 | url: twttr.txt.htmlEscape(url) 312 | }; 313 | 314 | return stringSupplant("#{before}#{url}", d); 315 | } else { 316 | return all; 317 | } 318 | }); 319 | }; 320 | 321 | twttr.txt.extractMentions = function(text) { 322 | var screenNamesOnly = [], 323 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); 324 | 325 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 326 | var screenName = screenNamesWithIndices[i].screenName; 327 | screenNamesOnly.push(screenName); 328 | } 329 | 330 | return screenNamesOnly; 331 | }; 332 | 333 | twttr.txt.extractMentionsWithIndices = function(text) { 334 | if (!text) { 335 | return []; 336 | } 337 | 338 | var possibleScreenNames = [], 339 | position = 0; 340 | 341 | text.replace(twttr.txt.regexen.extractMentions, function(match, before, screenName, after) { 342 | if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { 343 | var startPosition = text.indexOf(screenName, position) - 1; 344 | position = startPosition + screenName.length + 1; 345 | possibleScreenNames.push({ 346 | screenName: screenName, 347 | indices: [startPosition, position] 348 | }); 349 | } 350 | }); 351 | 352 | return possibleScreenNames; 353 | }; 354 | 355 | twttr.txt.extractReplies = function(text) { 356 | if (!text) { 357 | return null; 358 | } 359 | 360 | var possibleScreenName = text.match(twttr.txt.regexen.extractReply); 361 | if (!possibleScreenName) { 362 | return null; 363 | } 364 | 365 | return possibleScreenName[1]; 366 | }; 367 | 368 | twttr.txt.extractUrls = function(text) { 369 | var urlsOnly = [], 370 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); 371 | 372 | for (var i = 0; i < urlsWithIndices.length; i++) { 373 | urlsOnly.push(urlsWithIndices[i].url); 374 | } 375 | 376 | return urlsOnly; 377 | }; 378 | 379 | twttr.txt.extractUrlsWithIndices = function(text) { 380 | if (!text) { 381 | return []; 382 | } 383 | 384 | var urls = [], 385 | position = 0; 386 | 387 | text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) { 388 | if (protocol || domain.match(twttr.txt.regexen.probableTld)) { 389 | var startPosition = text.indexOf(url, position), 390 | position = startPosition + url.length; 391 | 392 | urls.push({ 393 | url: ((!protocol || protocol.match(twttr.txt.regexen.www)) ? stringSupplant("http://#{url}", {url: url}) : url), 394 | indices: [startPosition, position] 395 | }); 396 | } 397 | }); 398 | 399 | return urls; 400 | }; 401 | 402 | twttr.txt.extractHashtags = function(text) { 403 | var hashtagsOnly = [], 404 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); 405 | 406 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 407 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 408 | } 409 | 410 | return hashtagsOnly; 411 | }; 412 | 413 | twttr.txt.extractHashtagsWithIndices = function(text) { 414 | if (!text) { 415 | return []; 416 | } 417 | 418 | var tags = [], 419 | position = 0; 420 | 421 | text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) { 422 | var startPosition = text.indexOf(hash + hashText, position); 423 | position = startPosition + hashText.length + 1; 424 | tags.push({ 425 | hashtag: hashText, 426 | indices: [startPosition, position] 427 | }); 428 | }); 429 | 430 | return tags; 431 | }; 432 | 433 | // this essentially does text.split(/<|>/) 434 | // except that won't work in IE, where empty strings are ommitted 435 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 436 | // but "<<".split("<") => ["", "", ""] 437 | twttr.txt.splitTags = function(text) { 438 | var firstSplits = text.split("<"), 439 | secondSplits, 440 | allSplits = [], 441 | split; 442 | 443 | for (var i = 0; i < firstSplits.length; i += 1) { 444 | split = firstSplits[i]; 445 | if (!split) { 446 | allSplits.push(""); 447 | } else { 448 | secondSplits = split.split(">"); 449 | for (var j = 0; j < secondSplits.length; j += 1) { 450 | allSplits.push(secondSplits[j]); 451 | } 452 | } 453 | } 454 | 455 | return allSplits; 456 | }; 457 | 458 | twttr.txt.hitHighlight = function(text, hits, options) { 459 | var defaultHighlightTag = "em"; 460 | 461 | hits = hits || []; 462 | options = options || {}; 463 | 464 | if (hits.length === 0) { 465 | return text; 466 | } 467 | 468 | var tagName = options.tag || defaultHighlightTag, 469 | tags = ["<" + tagName + ">", ""], 470 | chunks = twttr.txt.splitTags(text), 471 | split, 472 | i, 473 | j, 474 | result = "", 475 | chunkIndex = 0, 476 | chunk = chunks[0], 477 | prevChunksLen = 0, 478 | chunkCursor = 0, 479 | startInChunk = false, 480 | chunkChars = chunk, 481 | flatHits = [], 482 | index, 483 | hit, 484 | tag, 485 | placed, 486 | hitSpot; 487 | 488 | for (i = 0; i < hits.length; i += 1) { 489 | for (j = 0; j < hits[i].length; j += 1) { 490 | flatHits.push(hits[i][j]); 491 | } 492 | } 493 | 494 | for (index = 0; index < flatHits.length; index += 1) { 495 | hit = flatHits[index]; 496 | tag = tags[index % 2]; 497 | placed = false; 498 | 499 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 500 | result += chunkChars.slice(chunkCursor); 501 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 502 | result += tag; 503 | placed = true; 504 | } 505 | 506 | if (chunks[chunkIndex + 1]) { 507 | result += "<" + chunks[chunkIndex + 1] + ">"; 508 | } 509 | 510 | prevChunksLen += chunkChars.length; 511 | chunkCursor = 0; 512 | chunkIndex += 2; 513 | chunk = chunks[chunkIndex]; 514 | chunkChars = chunk; 515 | startInChunk = false; 516 | } 517 | 518 | if (!placed && chunk != null) { 519 | hitSpot = hit - prevChunksLen; 520 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 521 | chunkCursor = hitSpot; 522 | if (index % 2 === 0) { 523 | startInChunk = true; 524 | } 525 | } 526 | } 527 | 528 | if (chunk != null) { 529 | if (chunkCursor < chunkChars.length) { 530 | result += chunkChars.slice(chunkCursor); 531 | } 532 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 533 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); 534 | } 535 | } 536 | 537 | return result; 538 | }; 539 | 540 | 541 | }()); -------------------------------------------------------------------------------- /pkg/twitter-text-1.0.5.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * twitter-text-js 1.0.5 3 | * 4 | * Copyright 2010 Twitter, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | * use this file except in compliance with the License. You may obtain a copy of 8 | * the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | * License for the specific language governing permissions and limitations under 16 | * the License. 17 | */ 18 | 19 | if (!window.twttr) { 20 | window.twttr = {}; 21 | } 22 | 23 | (function() { 24 | twttr.txt = {}; 25 | twttr.txt.regexen = {}; 26 | 27 | var HTML_ENTITIES = { 28 | '&': '&', 29 | '>': '>', 30 | '<': '<', 31 | '"': '"', 32 | "'": ' ' 33 | }; 34 | 35 | // HTML escaping 36 | twttr.txt.htmlEscape = function(text) { 37 | return text && text.replace(/[&"'><]/g, function(character) { 38 | return HTML_ENTITIES[character]; 39 | }); 40 | }; 41 | 42 | // Builds a RegExp 43 | function regexSupplant(regex, flags) { 44 | flags = flags || ""; 45 | if (typeof regex !== "string") { 46 | if (regex.global && flags.indexOf("g") < 0) { 47 | flags += "g"; 48 | } 49 | if (regex.ignoreCase && flags.indexOf("i") < 0) { 50 | flags += "i"; 51 | } 52 | if (regex.multiline && flags.indexOf("m") < 0) { 53 | flags += "m"; 54 | } 55 | 56 | regex = regex.source; 57 | } 58 | 59 | return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { 60 | var newRegex = twttr.txt.regexen[name] || ""; 61 | if (typeof newRegex !== "string") { 62 | newRegex = newRegex.source; 63 | } 64 | return newRegex; 65 | }), flags); 66 | } 67 | 68 | // simple string interpolation 69 | function stringSupplant(str, values) { 70 | return str.replace(/#\{(\w+)\}/g, function(match, name) { 71 | return values[name] || ""; 72 | }); 73 | } 74 | 75 | // Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand 76 | // to access both the list of characters and a pattern suitible for use with String#split 77 | // Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE 78 | var fromCode = String.fromCharCode; 79 | var UNICODE_SPACES = [ 80 | fromCode(0x0020), // White_Space # Zs SPACE 81 | fromCode(0x0085), // White_Space # Cc 82 | fromCode(0x00A0), // White_Space # Zs NO-BREAK SPACE 83 | fromCode(0x1680), // White_Space # Zs OGHAM SPACE MARK 84 | fromCode(0x180E), // White_Space # Zs MONGOLIAN VOWEL SEPARATOR 85 | fromCode(0x2028), // White_Space # Zl LINE SEPARATOR 86 | fromCode(0x2029), // White_Space # Zp PARAGRAPH SEPARATOR 87 | fromCode(0x202F), // White_Space # Zs NARROW NO-BREAK SPACE 88 | fromCode(0x205F), // White_Space # Zs MEDIUM MATHEMATICAL SPACE 89 | fromCode(0x3000) // White_Space # Zs IDEOGRAPHIC SPACE 90 | ]; 91 | 92 | for (var i = 0x009; i <= 0x000D; i++) { // White_Space # Cc [5] .. 93 | UNICODE_SPACES.push(String.fromCharCode(i)); 94 | } 95 | 96 | for (var i = 0x2000; i <= 0x200A; i++) { // White_Space # Zs [11] EN QUAD..HAIR SPACE 97 | UNICODE_SPACES.push(String.fromCharCode(i)); 98 | } 99 | 100 | twttr.txt.regexen.spaces = regexSupplant("[" + UNICODE_SPACES.join("") + "]"); 101 | twttr.txt.regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~/; 102 | twttr.txt.regexen.atSigns = /[@@]/; 103 | twttr.txt.regexen.extractMentions = regexSupplant(/(^|[^a-zA-Z0-9_])#{atSigns}([a-zA-Z0-9_]{1,20})(?=(.|$))/g); 104 | twttr.txt.regexen.extractReply = regexSupplant(/^(?:#{spaces})*#{atSigns}([a-zA-Z0-9_]{1,20})/); 105 | twttr.txt.regexen.listName = /[a-zA-Z][a-zA-Z0-9_\-\u0080-\u00ff]{0,24}/; 106 | 107 | // Latin accented characters (subtracted 0xD7 from the range, it's a confusable multiplication sign. Looks like "x") 108 | twttr.txt.regexen.latinAccentChars = regexSupplant("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ\\303\\277"); 109 | twttr.txt.regexen.latenAccents = regexSupplant(/[#{latinAccentChars}]+/); 110 | 111 | twttr.txt.regexen.endScreenNameMatch = regexSupplant(/^(?:#{atSigns}|[#{latinAccentChars}]|:\/\/)/); 112 | 113 | // Characters considered valid in a hashtag but not at the beginning, where only a-z and 0-9 are valid. 114 | twttr.txt.regexen.hashtagCharacters = regexSupplant(/[a-z0-9_#{latinAccentChars}]/i); 115 | twttr.txt.regexen.autoLinkHashtags = regexSupplant(/(^|[^0-9A-Z&\/\?]+)(#|#)([0-9A-Z_]*[A-Z_]+#{hashtagCharacters}*)/gi); 116 | twttr.txt.regexen.autoLinkUsernamesOrLists = /(^|[^a-zA-Z0-9_]|RT:?)([@@]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_\-]{0,24})?/g; 117 | twttr.txt.regexen.autoLinkEmoticon = /(8\-\#|8\-E|\+\-\(|\`\@|\`O|\<\|:~\(|\}:o\{|:\-\[|\>o\<|X\-\/|\[:-\]\-I\-|\/\/\/\/Ö\\\\\\\\|\(\|:\|\/\)|∑:\*\)|\( \| \))/g; 118 | 119 | // URL related hash regex collection 120 | twttr.txt.regexen.validPrecedingChars = regexSupplant(/(?:[^-\/"':!=A-Za-z0-9_@@]|^|\:)/); 121 | twttr.txt.regexen.validDomain = regexSupplant(/(?:[^#{punct}\s][\.-](?=[^#{punct}\s])|[^#{punct}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?/i); 122 | 123 | // For protocol-less URLs, we'll accept them if they end in one of a handful of likely TLDs 124 | twttr.txt.regexen.probableTld = /^(.*?)((?:[a-z0-9_\.\-]+)\.(?:com|net|org|gov|edu))$/i; 125 | 126 | twttr.txt.regexen.www = /www\./i; 127 | 128 | twttr.txt.regexen.validGeneralUrlPathChars = /[a-z0-9!\*';:=\+\$\/%#\[\]\-_,~]/i; 129 | // Allow URL paths to contain balanced parens 130 | // 1. Used in Wikipedia URLs like /Primer_(film) 131 | // 2. Used in IIS sessions like /S(dfd346)/ 132 | twttr.txt.regexen.wikipediaDisambiguation = regexSupplant(/(?:\(#{validGeneralUrlPathChars}+\))/i); 133 | // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user 134 | twttr.txt.regexen.validUrlPathChars = regexSupplant(/(?:#{wikipediaDisambiguation}|@#{validGeneralUrlPathChars}+\/|[\.\,]?#{validGeneralUrlPathChars})/i); 135 | 136 | // Valid end-of-path chracters (so /foo. does not gobble the period). 137 | // 1. Allow =&# for empty URL parameters and other URL-join artifacts 138 | twttr.txt.regexen.validUrlPathEndingChars = /[a-z0-9=#\/]/i; 139 | twttr.txt.regexen.validUrlQueryChars = /[a-z0-9!\*'\(\);:&=\+\$\/%#\[\]\-_\.,~]/i; 140 | twttr.txt.regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i; 141 | twttr.txt.regexen.validUrl = regexSupplant( 142 | '(' + // $1 total match 143 | '(#{validPrecedingChars})' + // $2 Preceeding chracter 144 | '(' + // $3 URL 145 | '((?:https?:\\/\\/|www\\.)?)' + // $4 Protocol or beginning 146 | '(#{validDomain})' + // $5 Domain(s) and optional post number 147 | '(' + // $6 URL Path 148 | '\\/#{validUrlPathChars}*' + 149 | '#{validUrlPathEndingChars}?' + 150 | ')?' + 151 | '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String 152 | ')' + 153 | ')' 154 | , "gi"); 155 | 156 | // Default CSS class for auto-linked URLs 157 | var DEFAULT_URL_CLASS = "tweet-url"; 158 | // Default CSS class for auto-linked lists (along with the url class) 159 | var DEFAULT_LIST_CLASS = "list-slug"; 160 | // Default CSS class for auto-linked usernames (along with the url class) 161 | var DEFAULT_USERNAME_CLASS = "username"; 162 | // Default CSS class for auto-linked hashtags (along with the url class) 163 | var DEFAULT_HASHTAG_CLASS = "hashtag"; 164 | // HTML attribute for robot nofollow behavior (default) 165 | var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\""; 166 | 167 | // Simple object cloning function for simple objects 168 | function clone(o) { 169 | var r = {}; 170 | for (var k in o) { 171 | if (o.hasOwnProperty(k)) { 172 | r[k] = o[k]; 173 | } 174 | } 175 | 176 | return r; 177 | } 178 | 179 | twttr.txt.autoLink = function(text, options) { 180 | options = clone(options || {}); 181 | return twttr.txt.autoLinkUsernamesOrLists( 182 | twttr.txt.autoLinkUrlsCustom( 183 | twttr.txt.autoLinkHashtags(text, options), 184 | options), 185 | options); 186 | }; 187 | 188 | 189 | twttr.txt.autoLinkUsernamesOrLists = function(text, options) { 190 | options = clone(options || {}); 191 | 192 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 193 | options.listClass = options.listClass || DEFAULT_LIST_CLASS; 194 | options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS; 195 | options.usernameUrlBase = options.usernameUrlBase || "http://twitter.com/"; 196 | options.listUrlBase = options.listUrlBase || "http://twitter.com/"; 197 | if (!options.suppressNoFollow) { 198 | var extraHtml = HTML_ATTR_NO_FOLLOW; 199 | } 200 | 201 | var newText = "", 202 | splitText = twttr.txt.splitTags(text); 203 | 204 | for (var index = 0; index < splitText.length; index++) { 205 | var chunk = splitText[index]; 206 | 207 | if (index !== 0) { 208 | newText += ((index % 2 === 0) ? ">" : "<"); 209 | } 210 | 211 | if (index % 4 !== 0) { 212 | newText += chunk; 213 | } else { 214 | newText += chunk.replace(twttr.txt.regexen.autoLinkUsernamesOrLists, function(match, before, at, user, slashListname, offset, chunk) { 215 | var after = chunk.slice(offset + match.length); 216 | 217 | var d = { 218 | before: before, 219 | at: at, 220 | user: twttr.txt.htmlEscape(user), 221 | slashListname: twttr.txt.htmlEscape(slashListname), 222 | extraHtml: extraHtml, 223 | chunk: twttr.txt.htmlEscape(chunk) 224 | }; 225 | for (var k in options) { 226 | if (options.hasOwnProperty(k)) { 227 | d[k] = options[k]; 228 | } 229 | } 230 | 231 | if (slashListname && !options.suppressLists) { 232 | // the link is a list 233 | var list = d.chunk = stringSupplant("#{user}#{slashListname}", d); 234 | d.list = twttr.txt.htmlEscape(list.toLowerCase()); 235 | return stringSupplant("#{before}#{at}#{chunk}", d); 236 | } else { 237 | if (after && after.match(twttr.txt.regexen.endScreenNameMatch)) { 238 | // Followed by something that means we don't autolink 239 | return match; 240 | } else { 241 | // this is a screen name 242 | d.chunk = twttr.txt.htmlEscape(user); 243 | d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : ""; 244 | return stringSupplant("#{before}#{at}#{chunk}", d); 245 | } 246 | } 247 | }); 248 | } 249 | } 250 | 251 | return newText; 252 | }; 253 | 254 | twttr.txt.autoLinkHashtags = function(text, options) { 255 | options = clone(options || {}); 256 | options.urlClass = options.urlClass || DEFAULT_URL_CLASS; 257 | options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS; 258 | options.hashtagUrlBase = options.hashtagUrlBase || "http://twitter.com/search?q=%23"; 259 | if (!options.suppressNoFollow) { 260 | var extraHtml = HTML_ATTR_NO_FOLLOW; 261 | } 262 | 263 | return text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, text) { 264 | var d = { 265 | before: before, 266 | hash: twttr.txt.htmlEscape(hash), 267 | text: twttr.txt.htmlEscape(text), 268 | extraHtml: extraHtml 269 | }; 270 | 271 | for (var k in options) { 272 | if (options.hasOwnProperty(k)) { 273 | d[k] = options[k]; 274 | } 275 | } 276 | 277 | return stringSupplant("#{before}#{hash}#{text}", d); 278 | }); 279 | }; 280 | 281 | 282 | twttr.txt.autoLinkUrlsCustom = function(text, options) { 283 | options = clone(options || {}); 284 | if (!options.suppressNoFollow) { 285 | options.rel = "nofollow"; 286 | } 287 | if (options.urlClass) { 288 | options["class"] = options.urlClass; 289 | delete options.urlClass; 290 | } 291 | 292 | delete options.suppressNoFollow; 293 | delete options.suppressDataScreenName; 294 | 295 | return text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, queryString) { 296 | var tldComponents; 297 | 298 | if (protocol) { 299 | var htmlAttrs = ""; 300 | for (var k in options) { 301 | htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: k, v: options[k].toString().replace(/"/, """).replace(//, ">")}); 302 | } 303 | options.htmlAttrs || ""; 304 | var fullUrl = ((!protocol || protocol.match(twttr.txt.regexen.www)) ? stringSupplant("http://#{url}", {url: url}) : url); 305 | 306 | var d = { 307 | before: before, 308 | fullUrl: twttr.txt.htmlEscape(fullUrl), 309 | htmlAttrs: htmlAttrs, 310 | url: twttr.txt.htmlEscape(url) 311 | }; 312 | 313 | return stringSupplant("#{before}#{url}", d); 314 | } else if (tldComponents = all.match(twttr.txt.regexen.probableTld)) { 315 | var tldBefore = tldComponents[1]; 316 | var tldUrl = tldComponents[2]; 317 | var htmlAttrs = ""; 318 | for (var k in options) { 319 | htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", { 320 | k: k, 321 | v: twttr.txt.htmlEscape(options[k].toString()) 322 | }); 323 | } 324 | options.htmlAttrs || ""; 325 | 326 | var fullUrl = stringSupplant("http://#{url}", { 327 | url: tldUrl 328 | }); 329 | var prefix = (tldBefore == before ? before : stringSupplant("#{before}#{tldBefore}", { 330 | before: before, 331 | tldBefore: tldBefore 332 | })); 333 | 334 | var d = { 335 | before: prefix, 336 | fullUrl: twttr.txt.htmlEscape(fullUrl), 337 | htmlAttrs: htmlAttrs, 338 | url: twttr.txt.htmlEscape(tldUrl) 339 | }; 340 | 341 | return stringSupplant("#{before}#{url}", d); 342 | } else { 343 | return all; 344 | } 345 | }); 346 | }; 347 | 348 | twttr.txt.extractMentions = function(text) { 349 | var screenNamesOnly = [], 350 | screenNamesWithIndices = twttr.txt.extractMentionsWithIndices(text); 351 | 352 | for (var i = 0; i < screenNamesWithIndices.length; i++) { 353 | var screenName = screenNamesWithIndices[i].screenName; 354 | screenNamesOnly.push(screenName); 355 | } 356 | 357 | return screenNamesOnly; 358 | }; 359 | 360 | twttr.txt.extractMentionsWithIndices = function(text) { 361 | if (!text) { 362 | return []; 363 | } 364 | 365 | var possibleScreenNames = [], 366 | position = 0; 367 | 368 | text.replace(twttr.txt.regexen.extractMentions, function(match, before, screenName, after) { 369 | if (!after.match(twttr.txt.regexen.endScreenNameMatch)) { 370 | var startPosition = text.indexOf(screenName, position) - 1; 371 | position = startPosition + screenName.length + 1; 372 | possibleScreenNames.push({ 373 | screenName: screenName, 374 | indices: [startPosition, position] 375 | }); 376 | } 377 | }); 378 | 379 | return possibleScreenNames; 380 | }; 381 | 382 | twttr.txt.extractReplies = function(text) { 383 | if (!text) { 384 | return null; 385 | } 386 | 387 | var possibleScreenName = text.match(twttr.txt.regexen.extractReply); 388 | if (!possibleScreenName) { 389 | return null; 390 | } 391 | 392 | return possibleScreenName[1]; 393 | }; 394 | 395 | twttr.txt.extractUrls = function(text) { 396 | var urlsOnly = [], 397 | urlsWithIndices = twttr.txt.extractUrlsWithIndices(text); 398 | 399 | for (var i = 0; i < urlsWithIndices.length; i++) { 400 | urlsOnly.push(urlsWithIndices[i].url); 401 | } 402 | 403 | return urlsOnly; 404 | }; 405 | 406 | twttr.txt.extractUrlsWithIndices = function(text) { 407 | if (!text) { 408 | return []; 409 | } 410 | 411 | var urls = [], 412 | position = 0; 413 | 414 | text.replace(twttr.txt.regexen.validUrl, function(match, all, before, url, protocol, domain, path, query) { 415 | var tldComponents; 416 | 417 | if (protocol) { 418 | var startPosition = text.indexOf(url, position), 419 | position = startPosition + url.length; 420 | 421 | urls.push({ 422 | url: ((!protocol || protocol.match(twttr.txt.regexen.www)) ? stringSupplant("http://#{url}", { 423 | url: url 424 | }) : url), 425 | indices: [startPosition, position] 426 | }); 427 | } else if (tldComponents = all.match(twttr.txt.regexen.probableTld)) { 428 | var tldUrl = tldComponents[2]; 429 | var startPosition = text.indexOf(tldUrl, position), 430 | position = startPosition + tldUrl.length; 431 | 432 | urls.push({ 433 | url: stringSupplant("http://#{tldUrl}", { 434 | tldUrl: tldUrl 435 | }), 436 | indices: [startPosition, position] 437 | }); 438 | } 439 | }); 440 | 441 | return urls; 442 | }; 443 | 444 | twttr.txt.extractHashtags = function(text) { 445 | var hashtagsOnly = [], 446 | hashtagsWithIndices = twttr.txt.extractHashtagsWithIndices(text); 447 | 448 | for (var i = 0; i < hashtagsWithIndices.length; i++) { 449 | hashtagsOnly.push(hashtagsWithIndices[i].hashtag); 450 | } 451 | 452 | return hashtagsOnly; 453 | }; 454 | 455 | twttr.txt.extractHashtagsWithIndices = function(text) { 456 | if (!text) { 457 | return []; 458 | } 459 | 460 | var tags = [], 461 | position = 0; 462 | 463 | text.replace(twttr.txt.regexen.autoLinkHashtags, function(match, before, hash, hashText) { 464 | var startPosition = text.indexOf(hash + hashText, position); 465 | position = startPosition + hashText.length + 1; 466 | tags.push({ 467 | hashtag: hashText, 468 | indices: [startPosition, position] 469 | }); 470 | }); 471 | 472 | return tags; 473 | }; 474 | 475 | // this essentially does text.split(/<|>/) 476 | // except that won't work in IE, where empty strings are ommitted 477 | // so "<>".split(/<|>/) => [] in IE, but is ["", "", ""] in all others 478 | // but "<<".split("<") => ["", "", ""] 479 | twttr.txt.splitTags = function(text) { 480 | var firstSplits = text.split("<"), 481 | secondSplits, 482 | allSplits = [], 483 | split; 484 | 485 | for (var i = 0; i < firstSplits.length; i += 1) { 486 | split = firstSplits[i]; 487 | if (!split) { 488 | allSplits.push(""); 489 | } else { 490 | secondSplits = split.split(">"); 491 | for (var j = 0; j < secondSplits.length; j += 1) { 492 | allSplits.push(secondSplits[j]); 493 | } 494 | } 495 | } 496 | 497 | return allSplits; 498 | }; 499 | 500 | twttr.txt.hitHighlight = function(text, hits, options) { 501 | var defaultHighlightTag = "em"; 502 | 503 | hits = hits || []; 504 | options = options || {}; 505 | 506 | if (hits.length === 0) { 507 | return text; 508 | } 509 | 510 | var tagName = options.tag || defaultHighlightTag, 511 | tags = ["<" + tagName + ">", ""], 512 | chunks = twttr.txt.splitTags(text), 513 | split, 514 | i, 515 | j, 516 | result = "", 517 | chunkIndex = 0, 518 | chunk = chunks[0], 519 | prevChunksLen = 0, 520 | chunkCursor = 0, 521 | startInChunk = false, 522 | chunkChars = chunk, 523 | flatHits = [], 524 | index, 525 | hit, 526 | tag, 527 | placed, 528 | hitSpot; 529 | 530 | for (i = 0; i < hits.length; i += 1) { 531 | for (j = 0; j < hits[i].length; j += 1) { 532 | flatHits.push(hits[i][j]); 533 | } 534 | } 535 | 536 | for (index = 0; index < flatHits.length; index += 1) { 537 | hit = flatHits[index]; 538 | tag = tags[index % 2]; 539 | placed = false; 540 | 541 | while (chunk != null && hit >= prevChunksLen + chunk.length) { 542 | result += chunkChars.slice(chunkCursor); 543 | if (startInChunk && hit === prevChunksLen + chunkChars.length) { 544 | result += tag; 545 | placed = true; 546 | } 547 | 548 | if (chunks[chunkIndex + 1]) { 549 | result += "<" + chunks[chunkIndex + 1] + ">"; 550 | } 551 | 552 | prevChunksLen += chunkChars.length; 553 | chunkCursor = 0; 554 | chunkIndex += 2; 555 | chunk = chunks[chunkIndex]; 556 | chunkChars = chunk; 557 | startInChunk = false; 558 | } 559 | 560 | if (!placed && chunk != null) { 561 | hitSpot = hit - prevChunksLen; 562 | result += chunkChars.slice(chunkCursor, hitSpot) + tag; 563 | chunkCursor = hitSpot; 564 | if (index % 2 === 0) { 565 | startInChunk = true; 566 | } else { 567 | startInChunk = false; 568 | } 569 | } 570 | } 571 | 572 | if (chunk != null) { 573 | if (chunkCursor < chunkChars.length) { 574 | result += chunkChars.slice(chunkCursor); 575 | } 576 | for (index = chunkIndex + 1; index < chunks.length; index += 1) { 577 | result += (index % 2 === 0 ? chunks[index] : "<" + chunks[index] + ">"); 578 | } 579 | } 580 | 581 | return result; 582 | }; 583 | 584 | 585 | }()); -------------------------------------------------------------------------------- /lib/qunit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2009 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * and GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var QUnit = { 14 | 15 | // call on start of module test to prepend name to all tests 16 | module: function(name, testEnvironment) { 17 | config.currentModule = name; 18 | 19 | synchronize(function() { 20 | if ( config.currentModule ) { 21 | QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); 22 | } 23 | 24 | config.currentModule = name; 25 | config.moduleTestEnvironment = testEnvironment; 26 | config.moduleStats = { all: 0, bad: 0 }; 27 | 28 | QUnit.moduleStart( name, testEnvironment ); 29 | }); 30 | }, 31 | 32 | asyncTest: function(testName, expected, callback) { 33 | if ( arguments.length === 2 ) { 34 | callback = expected; 35 | expected = 0; 36 | } 37 | 38 | QUnit.test(testName, expected, callback, true); 39 | }, 40 | 41 | test: function(testName, expected, callback, async) { 42 | var name = '' + testName + '', testEnvironment, testEnvironmentArg; 43 | 44 | if ( arguments.length === 2 ) { 45 | callback = expected; 46 | expected = null; 47 | } 48 | // is 2nd argument a testEnvironment? 49 | if ( expected && typeof expected === 'object') { 50 | testEnvironmentArg = expected; 51 | expected = null; 52 | } 53 | 54 | if ( config.currentModule ) { 55 | name = '' + config.currentModule + ": " + name; 56 | } 57 | 58 | if ( !validTest(config.currentModule + ": " + testName) ) { 59 | return; 60 | } 61 | 62 | synchronize(function() { 63 | 64 | testEnvironment = extend({ 65 | setup: function() {}, 66 | teardown: function() {} 67 | }, config.moduleTestEnvironment); 68 | if (testEnvironmentArg) { 69 | extend(testEnvironment,testEnvironmentArg); 70 | } 71 | 72 | QUnit.testStart( testName, testEnvironment ); 73 | 74 | // allow utility functions to access the current test environment 75 | QUnit.current_testEnvironment = testEnvironment; 76 | 77 | config.assertions = []; 78 | config.expected = expected; 79 | 80 | var tests = id("qunit-tests"); 81 | if (tests) { 82 | var b = document.createElement("strong"); 83 | b.innerHTML = "Running " + name; 84 | var li = document.createElement("li"); 85 | li.appendChild( b ); 86 | li.id = "current-test-output"; 87 | tests.appendChild( li ) 88 | } 89 | 90 | try { 91 | if ( !config.pollution ) { 92 | saveGlobal(); 93 | } 94 | 95 | testEnvironment.setup.call(testEnvironment); 96 | } catch(e) { 97 | QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); 98 | } 99 | }); 100 | 101 | synchronize(function() { 102 | if ( async ) { 103 | QUnit.stop(); 104 | } 105 | 106 | try { 107 | callback.call(testEnvironment); 108 | } catch(e) { 109 | fail("Test " + name + " died, exception and test follows", e, callback); 110 | QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); 111 | // else next test will carry the responsibility 112 | saveGlobal(); 113 | 114 | // Restart the tests if they're blocking 115 | if ( config.blocking ) { 116 | start(); 117 | } 118 | } 119 | }); 120 | 121 | synchronize(function() { 122 | try { 123 | checkPollution(); 124 | testEnvironment.teardown.call(testEnvironment); 125 | } catch(e) { 126 | QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); 127 | } 128 | }); 129 | 130 | synchronize(function() { 131 | try { 132 | QUnit.reset(); 133 | } catch(e) { 134 | fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, QUnit.reset); 135 | } 136 | 137 | if ( config.expected && config.expected != config.assertions.length ) { 138 | QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); 139 | } 140 | 141 | var good = 0, bad = 0, 142 | tests = id("qunit-tests"); 143 | 144 | config.stats.all += config.assertions.length; 145 | config.moduleStats.all += config.assertions.length; 146 | 147 | if ( tests ) { 148 | var ol = document.createElement("ol"); 149 | 150 | for ( var i = 0; i < config.assertions.length; i++ ) { 151 | var assertion = config.assertions[i]; 152 | 153 | var li = document.createElement("li"); 154 | li.className = assertion.result ? "pass" : "fail"; 155 | li.innerHTML = assertion.message || "(no message)"; 156 | ol.appendChild( li ); 157 | 158 | if ( assertion.result ) { 159 | good++; 160 | } else { 161 | bad++; 162 | config.stats.bad++; 163 | config.moduleStats.bad++; 164 | } 165 | } 166 | if (bad == 0) { 167 | ol.style.display = "none"; 168 | } 169 | 170 | var b = document.createElement("strong"); 171 | b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; 172 | 173 | addEvent(b, "click", function() { 174 | var next = b.nextSibling, display = next.style.display; 175 | next.style.display = display === "none" ? "block" : "none"; 176 | }); 177 | 178 | addEvent(b, "dblclick", function(e) { 179 | var target = e && e.target ? e.target : window.event.srcElement; 180 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 181 | target = target.parentNode; 182 | } 183 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 184 | window.location.search = "?" + encodeURIComponent(getText([target]).replace(/\(.+\)$/, "").replace(/(^\s*|\s*$)/g, "")); 185 | } 186 | }); 187 | 188 | var li = id("current-test-output"); 189 | li.id = ""; 190 | li.className = bad ? "fail" : "pass"; 191 | li.removeChild( li.firstChild ); 192 | li.appendChild( b ); 193 | li.appendChild( ol ); 194 | 195 | if ( bad ) { 196 | var toolbar = id("qunit-testrunner-toolbar"); 197 | if ( toolbar ) { 198 | toolbar.style.display = "block"; 199 | id("qunit-filter-pass").disabled = null; 200 | id("qunit-filter-missing").disabled = null; 201 | } 202 | } 203 | 204 | } else { 205 | for ( var i = 0; i < config.assertions.length; i++ ) { 206 | if ( !config.assertions[i].result ) { 207 | bad++; 208 | config.stats.bad++; 209 | config.moduleStats.bad++; 210 | } 211 | } 212 | } 213 | 214 | QUnit.testDone( testName, bad, config.assertions.length ); 215 | 216 | if ( !window.setTimeout && !config.queue.length ) { 217 | done(); 218 | } 219 | }); 220 | 221 | synchronize( done ); 222 | }, 223 | 224 | /** 225 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 226 | */ 227 | expect: function(asserts) { 228 | config.expected = asserts; 229 | }, 230 | 231 | /** 232 | * Asserts true. 233 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 234 | */ 235 | ok: function(a, msg) { 236 | msg = escapeHtml(msg); 237 | QUnit.log(a, msg); 238 | 239 | config.assertions.push({ 240 | result: !!a, 241 | message: msg 242 | }); 243 | }, 244 | 245 | /** 246 | * Checks that the first two arguments are equal, with an optional message. 247 | * Prints out both actual and expected values. 248 | * 249 | * Prefered to ok( actual == expected, message ) 250 | * 251 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 252 | * 253 | * @param Object actual 254 | * @param Object expected 255 | * @param String message (optional) 256 | */ 257 | equal: function(actual, expected, message) { 258 | push(expected == actual, actual, expected, message); 259 | }, 260 | 261 | notEqual: function(actual, expected, message) { 262 | push(expected != actual, actual, expected, message); 263 | }, 264 | 265 | deepEqual: function(actual, expected, message) { 266 | push(QUnit.equiv(actual, expected), actual, expected, message); 267 | }, 268 | 269 | notDeepEqual: function(actual, expected, message) { 270 | push(!QUnit.equiv(actual, expected), actual, expected, message); 271 | }, 272 | 273 | strictEqual: function(actual, expected, message) { 274 | push(expected === actual, actual, expected, message); 275 | }, 276 | 277 | notStrictEqual: function(actual, expected, message) { 278 | push(expected !== actual, actual, expected, message); 279 | }, 280 | 281 | raises: function(fn, message) { 282 | try { 283 | fn(); 284 | ok( false, message ); 285 | } 286 | catch (e) { 287 | ok( true, message ); 288 | } 289 | }, 290 | 291 | start: function() { 292 | // A slight delay, to avoid any current callbacks 293 | if ( window.setTimeout ) { 294 | window.setTimeout(function() { 295 | if ( config.timeout ) { 296 | clearTimeout(config.timeout); 297 | } 298 | 299 | config.blocking = false; 300 | process(); 301 | }, 13); 302 | } else { 303 | config.blocking = false; 304 | process(); 305 | } 306 | }, 307 | 308 | stop: function(timeout) { 309 | config.blocking = true; 310 | 311 | if ( timeout && window.setTimeout ) { 312 | config.timeout = window.setTimeout(function() { 313 | QUnit.ok( false, "Test timed out" ); 314 | QUnit.start(); 315 | }, timeout); 316 | } 317 | } 318 | 319 | }; 320 | 321 | // Backwards compatibility, deprecated 322 | QUnit.equals = QUnit.equal; 323 | QUnit.same = QUnit.deepEqual; 324 | 325 | // Maintain internal state 326 | var config = { 327 | // The queue of tests to run 328 | queue: [], 329 | 330 | // block until document ready 331 | blocking: true 332 | }; 333 | 334 | // Load paramaters 335 | (function() { 336 | var location = window.location || { search: "", protocol: "file:" }, 337 | GETParams = location.search.slice(1).split('&'); 338 | 339 | for ( var i = 0; i < GETParams.length; i++ ) { 340 | GETParams[i] = decodeURIComponent( GETParams[i] ); 341 | if ( GETParams[i] === "noglobals" ) { 342 | GETParams.splice( i, 1 ); 343 | i--; 344 | config.noglobals = true; 345 | } else if ( GETParams[i].search('=') > -1 ) { 346 | GETParams.splice( i, 1 ); 347 | i--; 348 | } 349 | } 350 | 351 | // restrict modules/tests by get parameters 352 | config.filters = GETParams; 353 | 354 | // Figure out if we're running the tests from a server or not 355 | QUnit.isLocal = !!(location.protocol === 'file:'); 356 | })(); 357 | 358 | // Expose the API as global variables, unless an 'exports' 359 | // object exists, in that case we assume we're in CommonJS 360 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 361 | extend(window, QUnit); 362 | window.QUnit = QUnit; 363 | } else { 364 | extend(exports, QUnit); 365 | exports.QUnit = QUnit; 366 | } 367 | 368 | // define these after exposing globals to keep them in these QUnit namespace only 369 | extend(QUnit, { 370 | config: config, 371 | 372 | // Initialize the configuration options 373 | init: function() { 374 | extend(config, { 375 | stats: { all: 0, bad: 0 }, 376 | moduleStats: { all: 0, bad: 0 }, 377 | started: +new Date, 378 | updateRate: 1000, 379 | blocking: false, 380 | autostart: true, 381 | autorun: false, 382 | assertions: [], 383 | filters: [], 384 | queue: [] 385 | }); 386 | 387 | var tests = id("qunit-tests"), 388 | banner = id("qunit-banner"), 389 | result = id("qunit-testresult"); 390 | 391 | if ( tests ) { 392 | tests.innerHTML = ""; 393 | } 394 | 395 | if ( banner ) { 396 | banner.className = ""; 397 | } 398 | 399 | if ( result ) { 400 | result.parentNode.removeChild( result ); 401 | } 402 | }, 403 | 404 | /** 405 | * Resets the test setup. Useful for tests that modify the DOM. 406 | */ 407 | reset: function() { 408 | if ( window.jQuery ) { 409 | jQuery("#main, #qunit-fixture").html( config.fixture ); 410 | } 411 | }, 412 | 413 | /** 414 | * Trigger an event on an element. 415 | * 416 | * @example triggerEvent( document.body, "click" ); 417 | * 418 | * @param DOMElement elem 419 | * @param String type 420 | */ 421 | triggerEvent: function( elem, type, event ) { 422 | if ( document.createEvent ) { 423 | event = document.createEvent("MouseEvents"); 424 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 425 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 426 | elem.dispatchEvent( event ); 427 | 428 | } else if ( elem.fireEvent ) { 429 | elem.fireEvent("on"+type); 430 | } 431 | }, 432 | 433 | // Safe object type checking 434 | is: function( type, obj ) { 435 | return QUnit.objectType( obj ) == type; 436 | }, 437 | 438 | objectType: function( obj ) { 439 | if (typeof obj === "undefined") { 440 | return "undefined"; 441 | 442 | // consider: typeof null === object 443 | } 444 | if (obj === null) { 445 | return "null"; 446 | } 447 | 448 | var type = Object.prototype.toString.call( obj ) 449 | .match(/^\[object\s(.*)\]$/)[1] || ''; 450 | 451 | switch (type) { 452 | case 'Number': 453 | if (isNaN(obj)) { 454 | return "nan"; 455 | } else { 456 | return "number"; 457 | } 458 | case 'String': 459 | case 'Boolean': 460 | case 'Array': 461 | case 'Date': 462 | case 'RegExp': 463 | case 'Function': 464 | return type.toLowerCase(); 465 | } 466 | if (typeof obj === "object") { 467 | return "object"; 468 | } 469 | return undefined; 470 | }, 471 | 472 | // Logging callbacks 473 | begin: function() {}, 474 | done: function(failures, total) {}, 475 | log: function(result, message) {}, 476 | testStart: function(name, testEnvironment) {}, 477 | testDone: function(name, failures, total) {}, 478 | moduleStart: function(name, testEnvironment) {}, 479 | moduleDone: function(name, failures, total) {} 480 | }); 481 | 482 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 483 | config.autorun = true; 484 | } 485 | 486 | addEvent(window, "load", function() { 487 | QUnit.begin(); 488 | 489 | // Initialize the config, saving the execution queue 490 | var oldconfig = extend({}, config); 491 | QUnit.init(); 492 | extend(config, oldconfig); 493 | 494 | config.blocking = false; 495 | 496 | var userAgent = id("qunit-userAgent"); 497 | if ( userAgent ) { 498 | userAgent.innerHTML = navigator.userAgent; 499 | } 500 | var banner = id("qunit-header"); 501 | if ( banner ) { 502 | banner.innerHTML = '' + banner.innerHTML + ''; 503 | } 504 | 505 | var toolbar = id("qunit-testrunner-toolbar"); 506 | if ( toolbar ) { 507 | toolbar.style.display = "none"; 508 | 509 | var filter = document.createElement("input"); 510 | filter.type = "checkbox"; 511 | filter.id = "qunit-filter-pass"; 512 | filter.disabled = true; 513 | addEvent( filter, "click", function() { 514 | var li = document.getElementsByTagName("li"); 515 | for ( var i = 0; i < li.length; i++ ) { 516 | if ( li[i].className.indexOf("pass") > -1 ) { 517 | li[i].style.display = filter.checked ? "none" : ""; 518 | } 519 | } 520 | }); 521 | toolbar.appendChild( filter ); 522 | 523 | var label = document.createElement("label"); 524 | label.setAttribute("for", "qunit-filter-pass"); 525 | label.innerHTML = "Hide passed tests"; 526 | toolbar.appendChild( label ); 527 | 528 | var missing = document.createElement("input"); 529 | missing.type = "checkbox"; 530 | missing.id = "qunit-filter-missing"; 531 | missing.disabled = true; 532 | addEvent( missing, "click", function() { 533 | var li = document.getElementsByTagName("li"); 534 | for ( var i = 0; i < li.length; i++ ) { 535 | if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { 536 | li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; 537 | } 538 | } 539 | }); 540 | toolbar.appendChild( missing ); 541 | 542 | label = document.createElement("label"); 543 | label.setAttribute("for", "qunit-filter-missing"); 544 | label.innerHTML = "Hide missing tests (untested code is broken code)"; 545 | toolbar.appendChild( label ); 546 | } 547 | 548 | var main = id('main') || id('qunit-fixture'); 549 | if ( main ) { 550 | config.fixture = main.innerHTML; 551 | } 552 | 553 | if (config.autostart) { 554 | QUnit.start(); 555 | } 556 | }); 557 | 558 | function done() { 559 | if ( config.doneTimer && window.clearTimeout ) { 560 | window.clearTimeout( config.doneTimer ); 561 | config.doneTimer = null; 562 | } 563 | 564 | if ( config.queue.length ) { 565 | config.doneTimer = window.setTimeout(function(){ 566 | if ( !config.queue.length ) { 567 | done(); 568 | } else { 569 | synchronize( done ); 570 | } 571 | }, 13); 572 | 573 | return; 574 | } 575 | 576 | config.autorun = true; 577 | 578 | // Log the last module results 579 | if ( config.currentModule ) { 580 | QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); 581 | } 582 | 583 | var banner = id("qunit-banner"), 584 | tests = id("qunit-tests"), 585 | html = ['Tests completed in ', 586 | +new Date - config.started, ' milliseconds.
      ', 587 | '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); 588 | 589 | if ( banner ) { 590 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 591 | } 592 | 593 | if ( tests ) { 594 | var result = id("qunit-testresult"); 595 | 596 | if ( !result ) { 597 | result = document.createElement("p"); 598 | result.id = "qunit-testresult"; 599 | result.className = "result"; 600 | tests.parentNode.insertBefore( result, tests.nextSibling ); 601 | } 602 | 603 | result.innerHTML = html; 604 | } 605 | 606 | QUnit.done( config.stats.bad, config.stats.all ); 607 | } 608 | 609 | function validTest( name ) { 610 | var i = config.filters.length, 611 | run = false; 612 | 613 | if ( !i ) { 614 | return true; 615 | } 616 | 617 | while ( i-- ) { 618 | var filter = config.filters[i], 619 | not = filter.charAt(0) == '!'; 620 | 621 | if ( not ) { 622 | filter = filter.slice(1); 623 | } 624 | 625 | if ( name.indexOf(filter) !== -1 ) { 626 | return !not; 627 | } 628 | 629 | if ( not ) { 630 | run = true; 631 | } 632 | } 633 | 634 | return run; 635 | } 636 | 637 | function escapeHtml(s) { 638 | s = s === null ? "" : s + ""; 639 | return s.replace(/[\&"<>\\]/g, function(s) { 640 | switch(s) { 641 | case "&": return "&"; 642 | case "\\": return "\\\\"; 643 | case '"': return '\"'; 644 | case "<": return "<"; 645 | case ">": return ">"; 646 | default: return s; 647 | } 648 | }); 649 | } 650 | 651 | function push(result, actual, expected, message) { 652 | message = escapeHtml(message) || (result ? "okay" : "failed"); 653 | message = '' + message + ""; 654 | expected = escapeHtml(QUnit.jsDump.parse(expected)); 655 | actual = escapeHtml(QUnit.jsDump.parse(actual)); 656 | var output = message + ', expected: ' + expected + ''; 657 | if (actual != expected) { 658 | output += ' result: ' + actual + ', diff: ' + QUnit.diff(expected, actual); 659 | } 660 | 661 | // can't use ok, as that would double-escape messages 662 | QUnit.log(result, output); 663 | config.assertions.push({ 664 | result: !!result, 665 | message: output 666 | }); 667 | } 668 | 669 | function synchronize( callback ) { 670 | config.queue.push( callback ); 671 | 672 | if ( config.autorun && !config.blocking ) { 673 | process(); 674 | } 675 | } 676 | 677 | function process() { 678 | var start = (new Date()).getTime(); 679 | 680 | while ( config.queue.length && !config.blocking ) { 681 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 682 | config.queue.shift()(); 683 | 684 | } else { 685 | setTimeout( process, 13 ); 686 | break; 687 | } 688 | } 689 | } 690 | 691 | function saveGlobal() { 692 | config.pollution = []; 693 | 694 | if ( config.noglobals ) { 695 | for ( var key in window ) { 696 | config.pollution.push( key ); 697 | } 698 | } 699 | } 700 | 701 | function checkPollution( name ) { 702 | var old = config.pollution; 703 | saveGlobal(); 704 | 705 | var newGlobals = diff( old, config.pollution ); 706 | if ( newGlobals.length > 0 ) { 707 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 708 | config.expected++; 709 | } 710 | 711 | var deletedGlobals = diff( config.pollution, old ); 712 | if ( deletedGlobals.length > 0 ) { 713 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 714 | config.expected++; 715 | } 716 | } 717 | 718 | // returns a new Array with the elements that are in a but not in b 719 | function diff( a, b ) { 720 | var result = a.slice(); 721 | for ( var i = 0; i < result.length; i++ ) { 722 | for ( var j = 0; j < b.length; j++ ) { 723 | if ( result[i] === b[j] ) { 724 | result.splice(i, 1); 725 | i--; 726 | break; 727 | } 728 | } 729 | } 730 | return result; 731 | } 732 | 733 | function fail(message, exception, callback) { 734 | if ( typeof console !== "undefined" && console.error && console.warn ) { 735 | console.error(message); 736 | console.error(exception); 737 | console.warn(callback.toString()); 738 | 739 | } else if ( window.opera && opera.postError ) { 740 | opera.postError(message, exception, callback.toString); 741 | } 742 | } 743 | 744 | function extend(a, b) { 745 | for ( var prop in b ) { 746 | a[prop] = b[prop]; 747 | } 748 | 749 | return a; 750 | } 751 | 752 | function addEvent(elem, type, fn) { 753 | if ( elem.addEventListener ) { 754 | elem.addEventListener( type, fn, false ); 755 | } else if ( elem.attachEvent ) { 756 | elem.attachEvent( "on" + type, fn ); 757 | } else { 758 | fn(); 759 | } 760 | } 761 | 762 | function id(name) { 763 | return !!(typeof document !== "undefined" && document && document.getElementById) && 764 | document.getElementById( name ); 765 | } 766 | 767 | // Test for equality any JavaScript type. 768 | // Discussions and reference: http://philrathe.com/articles/equiv 769 | // Test suites: http://philrathe.com/tests/equiv 770 | // Author: Philippe Rathé 771 | QUnit.equiv = function () { 772 | 773 | var innerEquiv; // the real equiv function 774 | var callers = []; // stack to decide between skip/abort functions 775 | var parents = []; // stack to avoiding loops from circular referencing 776 | 777 | // Call the o related callback with the given arguments. 778 | function bindCallbacks(o, callbacks, args) { 779 | var prop = QUnit.objectType(o); 780 | if (prop) { 781 | if (QUnit.objectType(callbacks[prop]) === "function") { 782 | return callbacks[prop].apply(callbacks, args); 783 | } else { 784 | return callbacks[prop]; // or undefined 785 | } 786 | } 787 | } 788 | 789 | var callbacks = function () { 790 | 791 | // for string, boolean, number and null 792 | function useStrictEquality(b, a) { 793 | if (b instanceof a.constructor || a instanceof b.constructor) { 794 | // to catch short annotaion VS 'new' annotation of a declaration 795 | // e.g. var i = 1; 796 | // var j = new Number(1); 797 | return a == b; 798 | } else { 799 | return a === b; 800 | } 801 | } 802 | 803 | return { 804 | "string": useStrictEquality, 805 | "boolean": useStrictEquality, 806 | "number": useStrictEquality, 807 | "null": useStrictEquality, 808 | "undefined": useStrictEquality, 809 | 810 | "nan": function (b) { 811 | return isNaN(b); 812 | }, 813 | 814 | "date": function (b, a) { 815 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 816 | }, 817 | 818 | "regexp": function (b, a) { 819 | return QUnit.objectType(b) === "regexp" && 820 | a.source === b.source && // the regex itself 821 | a.global === b.global && // and its modifers (gmi) ... 822 | a.ignoreCase === b.ignoreCase && 823 | a.multiline === b.multiline; 824 | }, 825 | 826 | // - skip when the property is a method of an instance (OOP) 827 | // - abort otherwise, 828 | // initial === would have catch identical references anyway 829 | "function": function () { 830 | var caller = callers[callers.length - 1]; 831 | return caller !== Object && 832 | typeof caller !== "undefined"; 833 | }, 834 | 835 | "array": function (b, a) { 836 | var i, j, loop; 837 | var len; 838 | 839 | // b could be an object literal here 840 | if ( ! (QUnit.objectType(b) === "array")) { 841 | return false; 842 | } 843 | 844 | len = a.length; 845 | if (len !== b.length) { // safe and faster 846 | return false; 847 | } 848 | 849 | //track reference to avoid circular references 850 | parents.push(a); 851 | for (i = 0; i < len; i++) { 852 | loop = false; 853 | for(j=0;j= 0) { 998 | type = "array"; 999 | } else { 1000 | type = typeof obj; 1001 | } 1002 | return type; 1003 | }, 1004 | separator:function() { 1005 | return this.multiline ? this.HTML ? '
      ' : '\n' : this.HTML ? ' ' : ' '; 1006 | }, 1007 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1008 | if ( !this.multiline ) 1009 | return ''; 1010 | var chr = this.indentChar; 1011 | if ( this.HTML ) 1012 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1013 | return Array( this._depth_ + (extra||0) ).join(chr); 1014 | }, 1015 | up:function( a ) { 1016 | this._depth_ += a || 1; 1017 | }, 1018 | down:function( a ) { 1019 | this._depth_ -= a || 1; 1020 | }, 1021 | setParser:function( name, parser ) { 1022 | this.parsers[name] = parser; 1023 | }, 1024 | // The next 3 are exposed so you can use them 1025 | quote:quote, 1026 | literal:literal, 1027 | join:join, 1028 | // 1029 | _depth_: 1, 1030 | // This is the list of parsers, to modify them, use jsDump.setParser 1031 | parsers:{ 1032 | window: '[Window]', 1033 | document: '[Document]', 1034 | error:'[ERROR]', //when no parser is found, shouldn't happen 1035 | unknown: '[Unknown]', 1036 | 'null':'null', 1037 | undefined:'undefined', 1038 | 'function':function( fn ) { 1039 | var ret = 'function', 1040 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1041 | if ( name ) 1042 | ret += ' ' + name; 1043 | ret += '('; 1044 | 1045 | ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); 1046 | return join( ret, this.parse(fn,'functionCode'), '}' ); 1047 | }, 1048 | array: array, 1049 | nodelist: array, 1050 | arguments: array, 1051 | object:function( map ) { 1052 | var ret = [ ]; 1053 | this.up(); 1054 | for ( var key in map ) 1055 | ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); 1056 | this.down(); 1057 | return join( '{', ret, '}' ); 1058 | }, 1059 | node:function( node ) { 1060 | var open = this.HTML ? '<' : '<', 1061 | close = this.HTML ? '>' : '>'; 1062 | 1063 | var tag = node.nodeName.toLowerCase(), 1064 | ret = open + tag; 1065 | 1066 | for ( var a in this.DOMAttrs ) { 1067 | var val = node[this.DOMAttrs[a]]; 1068 | if ( val ) 1069 | ret += ' ' + a + '=' + this.parse( val, 'attribute' ); 1070 | } 1071 | return ret + close + open + '/' + tag + close; 1072 | }, 1073 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1074 | var l = fn.length; 1075 | if ( !l ) return ''; 1076 | 1077 | var args = Array(l); 1078 | while ( l-- ) 1079 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1080 | return ' ' + args.join(', ') + ' '; 1081 | }, 1082 | key:quote, //object calls it internally, the key part of an item in a map 1083 | functionCode:'[code]', //function calls it internally, it's the content of the function 1084 | attribute:quote, //node calls it internally, it's an html attribute value 1085 | string:quote, 1086 | date:quote, 1087 | regexp:literal, //regex 1088 | number:literal, 1089 | 'boolean':literal 1090 | }, 1091 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1092 | id:'id', 1093 | name:'name', 1094 | 'class':'className' 1095 | }, 1096 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1097 | indentChar:' ',//indentation unit 1098 | multiline:false //if true, items in a collection, are separated by a \n, else just a space. 1099 | }; 1100 | 1101 | return jsDump; 1102 | })(); 1103 | 1104 | // from Sizzle.js 1105 | function getText( elems ) { 1106 | var ret = "", elem; 1107 | 1108 | for ( var i = 0; elems[i]; i++ ) { 1109 | elem = elems[i]; 1110 | 1111 | // Get the text from text nodes and CDATA nodes 1112 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1113 | ret += elem.nodeValue; 1114 | 1115 | // Traverse everything else, except comment nodes 1116 | } else if ( elem.nodeType !== 8 ) { 1117 | ret += getText( elem.childNodes ); 1118 | } 1119 | } 1120 | 1121 | return ret; 1122 | }; 1123 | 1124 | /* 1125 | * Javascript Diff Algorithm 1126 | * By John Resig (http://ejohn.org/) 1127 | * Modified by Chu Alan "sprite" 1128 | * 1129 | * Released under the MIT license. 1130 | * 1131 | * More Info: 1132 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1133 | * 1134 | * Usage: QUnit.diff(expected, actual) 1135 | * 1136 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1137 | */ 1138 | QUnit.diff = (function() { 1139 | function diff(o, n){ 1140 | var ns = new Object(); 1141 | var os = new Object(); 1142 | 1143 | for (var i = 0; i < n.length; i++) { 1144 | if (ns[n[i]] == null) 1145 | ns[n[i]] = { 1146 | rows: new Array(), 1147 | o: null 1148 | }; 1149 | ns[n[i]].rows.push(i); 1150 | } 1151 | 1152 | for (var i = 0; i < o.length; i++) { 1153 | if (os[o[i]] == null) 1154 | os[o[i]] = { 1155 | rows: new Array(), 1156 | n: null 1157 | }; 1158 | os[o[i]].rows.push(i); 1159 | } 1160 | 1161 | for (var i in ns) { 1162 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1163 | n[ns[i].rows[0]] = { 1164 | text: n[ns[i].rows[0]], 1165 | row: os[i].rows[0] 1166 | }; 1167 | o[os[i].rows[0]] = { 1168 | text: o[os[i].rows[0]], 1169 | row: ns[i].rows[0] 1170 | }; 1171 | } 1172 | } 1173 | 1174 | for (var i = 0; i < n.length - 1; i++) { 1175 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1176 | n[i + 1] == o[n[i].row + 1]) { 1177 | n[i + 1] = { 1178 | text: n[i + 1], 1179 | row: n[i].row + 1 1180 | }; 1181 | o[n[i].row + 1] = { 1182 | text: o[n[i].row + 1], 1183 | row: i + 1 1184 | }; 1185 | } 1186 | } 1187 | 1188 | for (var i = n.length - 1; i > 0; i--) { 1189 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1190 | n[i - 1] == o[n[i].row - 1]) { 1191 | n[i - 1] = { 1192 | text: n[i - 1], 1193 | row: n[i].row - 1 1194 | }; 1195 | o[n[i].row - 1] = { 1196 | text: o[n[i].row - 1], 1197 | row: i - 1 1198 | }; 1199 | } 1200 | } 1201 | 1202 | return { 1203 | o: o, 1204 | n: n 1205 | }; 1206 | } 1207 | 1208 | return function(o, n){ 1209 | o = o.replace(/\s+$/, ''); 1210 | n = n.replace(/\s+$/, ''); 1211 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1212 | 1213 | var str = ""; 1214 | 1215 | var oSpace = o.match(/\s+/g); 1216 | if (oSpace == null) { 1217 | oSpace = [" "]; 1218 | } 1219 | else { 1220 | oSpace.push(" "); 1221 | } 1222 | var nSpace = n.match(/\s+/g); 1223 | if (nSpace == null) { 1224 | nSpace = [" "]; 1225 | } 1226 | else { 1227 | nSpace.push(" "); 1228 | } 1229 | 1230 | if (out.n.length == 0) { 1231 | for (var i = 0; i < out.o.length; i++) { 1232 | str += '' + out.o[i] + oSpace[i] + ""; 1233 | } 1234 | } 1235 | else { 1236 | if (out.n[0].text == null) { 1237 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1238 | str += '' + out.o[n] + oSpace[n] + ""; 1239 | } 1240 | } 1241 | 1242 | for (var i = 0; i < out.n.length; i++) { 1243 | if (out.n[i].text == null) { 1244 | str += '' + out.n[i] + nSpace[i] + ""; 1245 | } 1246 | else { 1247 | var pre = ""; 1248 | 1249 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1250 | pre += '' + out.o[n] + oSpace[n] + ""; 1251 | } 1252 | str += " " + out.n[i].text + nSpace[i] + pre; 1253 | } 1254 | } 1255 | } 1256 | 1257 | return str; 1258 | } 1259 | })(); 1260 | 1261 | })(this); 1262 | --------------------------------------------------------------------------------