murlsh 0.11.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.htaccess +2 -0
- data/README.textile +3 -33
- data/Rakefile +12 -5
- data/VERSION +1 -1
- data/config.ru +10 -4
- data/config.yaml +6 -12
- data/lib/murlsh/dispatch.rb +0 -6
- data/lib/murlsh/img_store.rb +36 -0
- data/lib/murlsh/markup.rb +0 -1
- data/lib/murlsh/plugin.rb +2 -6
- data/lib/murlsh/uri_ask.rb +66 -22
- data/lib/murlsh/url.rb +0 -18
- data/lib/murlsh/url_body.rb +15 -23
- data/lib/murlsh/url_server.rb +5 -5
- data/murlsh.gemspec +44 -19
- data/plugins/add_post_50_update_feed.rb +17 -3
- data/plugins/add_post_50_update_podcast.rb +46 -0
- data/plugins/add_post_50_update_rss.rb +18 -2
- data/plugins/add_post_60_notify_hubs.rb +3 -1
- data/plugins/add_pre_40_convert_mobile.rb +30 -0
- data/plugins/add_pre_50_lookup_content_type_title.rb +11 -4
- data/plugins/add_pre_60_flickr.rb +38 -0
- data/plugins/add_pre_60_github_title.rb +5 -1
- data/plugins/add_pre_60_google_code_title.rb +5 -2
- data/plugins/add_pre_60_imageshack.rb +31 -0
- data/plugins/add_pre_60_imgur.rb +32 -0
- data/plugins/add_pre_60_s3_image.rb +34 -0
- data/plugins/add_pre_60_twitter.rb +35 -0
- data/plugins/add_pre_60_vimeo.rb +35 -0
- data/plugins/add_pre_60_youtube.rb +31 -0
- data/plugins/html_parse_50_hpricot.rb +2 -0
- data/plugins/url_display_add_45_mp3.rb +30 -0
- data/plugins/url_display_add_50_hostrec.rb +38 -0
- data/plugins/url_display_add_55_content_type.rb +27 -0
- data/plugins/url_display_add_60_via.rb +52 -0
- data/plugins/url_display_add_65_time.rb +22 -0
- data/public/css/jquery.jgrowl.css +0 -3
- data/public/css/screen.css +0 -18
- data/public/img/thumb/README +0 -0
- data/public/js/jquery-1.4.3.min.js +166 -0
- data/public/js/js.js +62 -234
- data/public/js/twitter-text-1.0.3.js +538 -0
- data/spec/img_store_spec.rb +53 -0
- data/spec/uri_ask_spec.rb +14 -4
- metadata +139 -37
- data/lib/murlsh/flickr_server.rb +0 -55
- data/lib/murlsh/twitter_server.rb +0 -45
- data/lib/murlsh/unwrap_jsonp.rb +0 -15
- data/lib/murlsh/xhtml_response.rb +0 -20
- data/plugins/hostrec_50_redundant.rb +0 -14
- data/plugins/hostrec_60_skip.rb +0 -24
- data/plugins/time_50_ago.rb +0 -16
- data/plugins/via_50_domain.rb +0 -36
- data/public/js/jquery-1.4.2.min.js +0 -154
- data/spec/unwrap_json_spec.rb +0 -21
- data/spec/xhtml_response_spec.rb +0 -112
@@ -0,0 +1,538 @@
|
|
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 <control-0085>
|
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] <control-0009>..<control-000D>
|
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}<a class=\"#{urlClass} #{listClass}\" href=\"#{listUrlBase}#{list}\"#{extraHtml}>#{chunk}</a>", 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}<a class=\"#{urlClass} #{usernameClass}\" #{dataScreenName}href=\"#{usernameUrlBase}#{chunk}\"#{extraHtml}>#{chunk}</a>", 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}<a href=\"#{hashtagUrlBase}#{text}\" title=\"##{text}\" class=\"#{urlClass} #{hashtagClass}\"#{extraHtml}>#{hash}#{text}</a>", 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}<a href=\"#{fullUrl}\"#{htmlAttrs}>#{url}</a>", 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
|
+
}());
|
@@ -0,0 +1,53 @@
|
|
1
|
+
%w{
|
2
|
+
cgi
|
3
|
+
digest/sha1
|
4
|
+
fileutils
|
5
|
+
open-uri
|
6
|
+
tempfile
|
7
|
+
|
8
|
+
murlsh
|
9
|
+
}.each { |m| require m }
|
10
|
+
|
11
|
+
describe Murlsh::ImgStore do
|
12
|
+
|
13
|
+
before(:all) do
|
14
|
+
@thumb_dir = File.join(Dir::tmpdir, 'img_store_test')
|
15
|
+
FileUtils.mkdir_p(@thumb_dir)
|
16
|
+
@img_store = Murlsh::ImgStore.new(@thumb_dir)
|
17
|
+
end
|
18
|
+
|
19
|
+
describe :store do
|
20
|
+
|
21
|
+
context 'given a valid image url' do
|
22
|
+
|
23
|
+
before(:all) do
|
24
|
+
@image_url =
|
25
|
+
'http://static.mmb.s3.amazonaws.com/2010_10_8_bacon_pancakes.jpg'
|
26
|
+
@local_file = @img_store.store(@image_url)
|
27
|
+
@local_path = File.join(@thumb_dir, @local_file)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should return the correct filename' do
|
31
|
+
@local_file.should == CGI.escape(@image_url)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should create a local file with the correct contents' do
|
35
|
+
sha1 = Digest::SHA1.hexdigest(open(@local_path) { |f| f.read })
|
36
|
+
sha1.should == '2749b80537cbf15f1c432c576b4d9e109a8ab565'
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'given an image url with an invalid path' do
|
42
|
+
|
43
|
+
it 'should raise OpenURI::HTTPError 404 Not Found' do
|
44
|
+
lambda {
|
45
|
+
@img_store.store('http://matthewm.boedicker.org/does_not_exist') }.
|
46
|
+
should raise_error(OpenURI::HTTPError, '404 Not Found')
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/spec/uri_ask_spec.rb
CHANGED
@@ -49,9 +49,14 @@ describe Murlsh::UriAsk do
|
|
49
49
|
content_type('http://x.boedicker.org/', :failproof => true).should be_empty
|
50
50
|
end
|
51
51
|
|
52
|
-
it 'should raise a
|
52
|
+
it 'should raise a SocketError when getting the content type of a URI with an invalid hostname when given failproof option false' do
|
53
53
|
lambda { content_type('http://x.boedicker.org/', :failproof => false)
|
54
|
-
}.should raise_error(
|
54
|
+
}.should raise_error(SocketError)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should raise an HTTPError when getting the content type of a URI with an invalid path when given failproof option false' do
|
58
|
+
lambda { content_type('http://boedicker.org/invalid', :failproof => false)
|
59
|
+
}.should raise_error(OpenURI::HTTPError)
|
55
60
|
end
|
56
61
|
|
57
62
|
it 'should limit redirects when getting content type' do
|
@@ -87,9 +92,14 @@ describe Murlsh::UriAsk do
|
|
87
92
|
).should == 'http://x.boedicker.org/'
|
88
93
|
end
|
89
94
|
|
90
|
-
it 'should raise a
|
95
|
+
it 'should raise a SocketError when trying to get the title of a URI with an invalid hostname when given failproof option false' do
|
91
96
|
lambda { title('http://x.boedicker.org/', :failproof => false)
|
92
|
-
}.should raise_error(
|
97
|
+
}.should raise_error(SocketError)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should raise an HTTPError when getting the title of a URI with an invalid path when given failproof option false' do
|
101
|
+
lambda { title('http://boedicker.org/invalid', :failproof => false)
|
102
|
+
}.should raise_error(OpenURI::HTTPError)
|
93
103
|
end
|
94
104
|
|
95
105
|
end
|