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 | ["&", "&"],
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(/, "<").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 + ">", "" + 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(/, "<").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 + ">", "" + 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(/, "<").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 + ">", "" + 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(/, "<").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 + ">", "" + 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(/, "<").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 + ">", "" + 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(/, "<").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 + ">", "" + 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(/, "<").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 + ">", "" + 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(/, "<").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 + ">", "" + 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 |
--------------------------------------------------------------------------------