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.
Files changed (56) hide show
  1. data/.htaccess +2 -0
  2. data/README.textile +3 -33
  3. data/Rakefile +12 -5
  4. data/VERSION +1 -1
  5. data/config.ru +10 -4
  6. data/config.yaml +6 -12
  7. data/lib/murlsh/dispatch.rb +0 -6
  8. data/lib/murlsh/img_store.rb +36 -0
  9. data/lib/murlsh/markup.rb +0 -1
  10. data/lib/murlsh/plugin.rb +2 -6
  11. data/lib/murlsh/uri_ask.rb +66 -22
  12. data/lib/murlsh/url.rb +0 -18
  13. data/lib/murlsh/url_body.rb +15 -23
  14. data/lib/murlsh/url_server.rb +5 -5
  15. data/murlsh.gemspec +44 -19
  16. data/plugins/add_post_50_update_feed.rb +17 -3
  17. data/plugins/add_post_50_update_podcast.rb +46 -0
  18. data/plugins/add_post_50_update_rss.rb +18 -2
  19. data/plugins/add_post_60_notify_hubs.rb +3 -1
  20. data/plugins/add_pre_40_convert_mobile.rb +30 -0
  21. data/plugins/add_pre_50_lookup_content_type_title.rb +11 -4
  22. data/plugins/add_pre_60_flickr.rb +38 -0
  23. data/plugins/add_pre_60_github_title.rb +5 -1
  24. data/plugins/add_pre_60_google_code_title.rb +5 -2
  25. data/plugins/add_pre_60_imageshack.rb +31 -0
  26. data/plugins/add_pre_60_imgur.rb +32 -0
  27. data/plugins/add_pre_60_s3_image.rb +34 -0
  28. data/plugins/add_pre_60_twitter.rb +35 -0
  29. data/plugins/add_pre_60_vimeo.rb +35 -0
  30. data/plugins/add_pre_60_youtube.rb +31 -0
  31. data/plugins/html_parse_50_hpricot.rb +2 -0
  32. data/plugins/url_display_add_45_mp3.rb +30 -0
  33. data/plugins/url_display_add_50_hostrec.rb +38 -0
  34. data/plugins/url_display_add_55_content_type.rb +27 -0
  35. data/plugins/url_display_add_60_via.rb +52 -0
  36. data/plugins/url_display_add_65_time.rb +22 -0
  37. data/public/css/jquery.jgrowl.css +0 -3
  38. data/public/css/screen.css +0 -18
  39. data/public/img/thumb/README +0 -0
  40. data/public/js/jquery-1.4.3.min.js +166 -0
  41. data/public/js/js.js +62 -234
  42. data/public/js/twitter-text-1.0.3.js +538 -0
  43. data/spec/img_store_spec.rb +53 -0
  44. data/spec/uri_ask_spec.rb +14 -4
  45. metadata +139 -37
  46. data/lib/murlsh/flickr_server.rb +0 -55
  47. data/lib/murlsh/twitter_server.rb +0 -45
  48. data/lib/murlsh/unwrap_jsonp.rb +0 -15
  49. data/lib/murlsh/xhtml_response.rb +0 -20
  50. data/plugins/hostrec_50_redundant.rb +0 -14
  51. data/plugins/hostrec_60_skip.rb +0 -24
  52. data/plugins/time_50_ago.rb +0 -16
  53. data/plugins/via_50_domain.rb +0 -36
  54. data/public/js/jquery-1.4.2.min.js +0 -154
  55. data/spec/unwrap_json_spec.rb +0 -21
  56. 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
+ '<': '&lt;',
31
+ '"': '&quot;',
32
+ "'": '&#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|\&lt;\|:~\(|\}:o\{|:\-\[|\&gt;o\&lt;|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(/"/, "&quot;").replace(/</, "&lt;").replace(/>/, "&gt;")});
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 NoMethodError when getting the content type of an invalid URI when given failproof option false' do
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(NoMethodError)
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 NoMethodError when trying to get the title of an invalid URI when given failproof option false' do
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(NoMethodError)
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