memcached-manager 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +13 -5
  2. data/.travis.yml +1 -1
  3. data/CONTRIBUTING.md +4 -4
  4. data/Dockerfile +7 -0
  5. data/Gemfile +0 -1
  6. data/Gemfile.lock +23 -19
  7. data/README.rdoc +8 -5
  8. data/Rakefile +6 -6
  9. data/VERSION +1 -1
  10. data/features/api/run_command.feature +6 -0
  11. data/features/api/search_memcached_keys.feature +6 -0
  12. data/features/step_definitions/api/create_memcached_key.rb +1 -1
  13. data/features/step_definitions/api/list_memcached_keys.rb +1 -0
  14. data/features/step_definitions/api/run_command.rb +12 -0
  15. data/features/step_definitions/api/search_memcached_keys.rb +6 -0
  16. data/features/step_definitions/webapp/create_memcached_key.rb +13 -7
  17. data/features/step_definitions/webapp/delete_memcached_key.rb +7 -13
  18. data/features/step_definitions/webapp/edit_configs.rb +7 -0
  19. data/features/support/env.rb +3 -2
  20. data/features/webapp/edit_configs.feature +9 -0
  21. data/features/webapp/edit_memcached_key.feature +1 -1
  22. data/fig.yml +13 -0
  23. data/githubpage_idea +23 -0
  24. data/lib/api.rb +25 -7
  25. data/lib/extensions.rb +6 -0
  26. data/lib/extensions/memcached_command.rb +14 -0
  27. data/lib/extensions/memcached_connection.rb +9 -0
  28. data/lib/extensions/memcached_inspector.rb +12 -5
  29. data/lib/extensions/memcached_settings.rb +2 -2
  30. data/lib/public/images/favicon.png +0 -0
  31. data/lib/public/images/glyphicons-halflings.png +0 -0
  32. data/lib/public/images/logo.png +0 -0
  33. data/lib/public/images/org-logo.png +0 -0
  34. data/lib/public/images/search.png +0 -0
  35. data/lib/public/javascripts/angular/controllers.js +48 -3
  36. data/lib/public/javascripts/angular/filters.js +85 -0
  37. data/lib/public/javascripts/angular/routes.js +26 -0
  38. data/lib/public/javascripts/angular/services/notification.js +41 -6
  39. data/lib/public/javascripts/angular/services/query_params_singleton.js +10 -0
  40. data/lib/public/javascripts/angular/services/resources.js +13 -0
  41. data/lib/public/javascripts/application.js +38 -1
  42. data/lib/public/javascripts/humanize.js +473 -0
  43. data/lib/public/javascripts/humanize_duration.js +329 -0
  44. data/lib/public/javascripts/jquery-terminal.js +4335 -0
  45. data/lib/public/javascripts/underscore.js +5 -0
  46. data/lib/public/stylesheets/app.css +196 -10
  47. data/lib/public/stylesheets/buttons.css +107 -0
  48. data/lib/public/stylesheets/inputs.css +119 -0
  49. data/lib/public/stylesheets/jquery-terminal.css +184 -0
  50. data/lib/public/stylesheets/media_queries.css +162 -0
  51. data/lib/public/templates/config.html.erb +8 -0
  52. data/lib/public/templates/edit.html.erb +5 -3
  53. data/lib/public/templates/keys.html.erb +42 -31
  54. data/lib/public/templates/new.html.erb +6 -4
  55. data/lib/public/templates/show.html.erb +1 -1
  56. data/lib/public/templates/stats.html.erb +1 -2
  57. data/lib/routes.rb +3 -0
  58. data/lib/views/index.erb +24 -3
  59. data/lib/views/layout.erb +14 -1
  60. data/memcached-manager.gemspec +31 -5
  61. data/spec/lib/extensions/api_response_spec.rb +9 -7
  62. data/spec/lib/extensions/memcached_command_spec.rb +13 -0
  63. data/spec/lib/extensions/memcached_connection_spec.rb +9 -4
  64. data/spec/lib/extensions/memcached_inspector_spec.rb +38 -17
  65. data/spec/lib/extensions/memcached_settings_spec.rb +16 -16
  66. data/spec/spec_helper.rb +2 -4
  67. metadata +48 -21
@@ -0,0 +1,473 @@
1
+ (function() {
2
+
3
+ // Baseline setup
4
+ // --------------
5
+
6
+ // Establish the root object, `window` in the browser, or `global` on the server.
7
+ var root = this;
8
+
9
+ // Save the previous value of the `humanize` variable.
10
+ var previousHumanize = root.humanize;
11
+
12
+ var humanize = {};
13
+
14
+ if (typeof exports !== 'undefined') {
15
+ if (typeof module !== 'undefined' && module.exports) {
16
+ exports = module.exports = humanize;
17
+ }
18
+ exports.humanize = humanize;
19
+ } else {
20
+ if (typeof define === 'function' && define.amd) {
21
+ define('humanize', function() {
22
+ return humanize;
23
+ });
24
+ }
25
+ root.humanize = humanize;
26
+ }
27
+
28
+ humanize.noConflict = function() {
29
+ root.humanize = previousHumanize;
30
+ return this;
31
+ };
32
+
33
+ humanize.pad = function(str, count, padChar, type) {
34
+ str += '';
35
+ if (!padChar) {
36
+ padChar = ' ';
37
+ } else if (padChar.length > 1) {
38
+ padChar = padChar.charAt(0);
39
+ }
40
+ type = (type === undefined) ? 'left' : 'right';
41
+
42
+ if (type === 'right') {
43
+ while (str.length < count) {
44
+ str = str + padChar;
45
+ }
46
+ } else {
47
+ // default to left
48
+ while (str.length < count) {
49
+ str = padChar + str;
50
+ }
51
+ }
52
+
53
+ return str;
54
+ };
55
+
56
+ // gets current unix time
57
+ humanize.time = function() {
58
+ return new Date().getTime() / 1000;
59
+ };
60
+
61
+ /**
62
+ * PHP-inspired date
63
+ */
64
+
65
+ /* jan feb mar apr may jun jul aug sep oct nov dec */
66
+ var dayTableCommon = [ 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ];
67
+ var dayTableLeap = [ 0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 ];
68
+ // var mtable_common[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
69
+ // static int ml_table_leap[13] = { 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
70
+
71
+
72
+ humanize.date = function(format, timestamp) {
73
+ var jsdate = ((timestamp === undefined) ? new Date() : // Not provided
74
+ (timestamp instanceof Date) ? new Date(timestamp) : // JS Date()
75
+ new Date(timestamp * 1000) // UNIX timestamp (auto-convert to int)
76
+ );
77
+
78
+ var formatChr = /\\?([a-z])/gi;
79
+ var formatChrCb = function (t, s) {
80
+ return f[t] ? f[t]() : s;
81
+ };
82
+
83
+ var shortDayTxt = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
84
+ var monthTxt = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
85
+
86
+ var f = {
87
+ /* Day */
88
+ // Day of month w/leading 0; 01..31
89
+ d: function () { return humanize.pad(f.j(), 2, '0'); },
90
+
91
+ // Shorthand day name; Mon..Sun
92
+ D: function () { return f.l().slice(0, 3); },
93
+
94
+ // Day of month; 1..31
95
+ j: function () { return jsdate.getDate(); },
96
+
97
+ // Full day name; Monday..Sunday
98
+ l: function () { return shortDayTxt[f.w()]; },
99
+
100
+ // ISO-8601 day of week; 1[Mon]..7[Sun]
101
+ N: function () { return f.w() || 7; },
102
+
103
+ // Ordinal suffix for day of month; st, nd, rd, th
104
+ S: function () {
105
+ var j = f.j();
106
+ return j > 4 && j < 21 ? 'th' : {1: 'st', 2: 'nd', 3: 'rd'}[j % 10] || 'th';
107
+ },
108
+
109
+ // Day of week; 0[Sun]..6[Sat]
110
+ w: function () { return jsdate.getDay(); },
111
+
112
+ // Day of year; 0..365
113
+ z: function () {
114
+ return (f.L() ? dayTableLeap[f.n()] : dayTableCommon[f.n()]) + f.j() - 1;
115
+ },
116
+
117
+ /* Week */
118
+ // ISO-8601 week number
119
+ W: function () {
120
+ // days between midweek of this week and jan 4
121
+ // (f.z() - f.N() + 1 + 3.5) - 3
122
+ var midWeekDaysFromJan4 = f.z() - f.N() + 1.5;
123
+ // 1 + number of weeks + rounded week
124
+ return humanize.pad(1 + Math.floor(Math.abs(midWeekDaysFromJan4) / 7) + (midWeekDaysFromJan4 % 7 > 3.5 ? 1 : 0), 2, '0');
125
+ },
126
+
127
+ /* Month */
128
+ // Full month name; January..December
129
+ F: function () { return monthTxt[jsdate.getMonth()]; },
130
+
131
+ // Month w/leading 0; 01..12
132
+ m: function () { return humanize.pad(f.n(), 2, '0'); },
133
+
134
+ // Shorthand month name; Jan..Dec
135
+ M: function () { return f.F().slice(0, 3); },
136
+
137
+ // Month; 1..12
138
+ n: function () { return jsdate.getMonth() + 1; },
139
+
140
+ // Days in month; 28..31
141
+ t: function () { return (new Date(f.Y(), f.n(), 0)).getDate(); },
142
+
143
+ /* Year */
144
+ // Is leap year?; 0 or 1
145
+ L: function () { return new Date(f.Y(), 1, 29).getMonth() === 1 ? 1 : 0; },
146
+
147
+ // ISO-8601 year
148
+ o: function () {
149
+ var n = f.n();
150
+ var W = f.W();
151
+ return f.Y() + (n === 12 && W < 9 ? -1 : n === 1 && W > 9);
152
+ },
153
+
154
+ // Full year; e.g. 1980..2010
155
+ Y: function () { return jsdate.getFullYear(); },
156
+
157
+ // Last two digits of year; 00..99
158
+ y: function () { return (String(f.Y())).slice(-2); },
159
+
160
+ /* Time */
161
+ // am or pm
162
+ a: function () { return jsdate.getHours() > 11 ? 'pm' : 'am'; },
163
+
164
+ // AM or PM
165
+ A: function () { return f.a().toUpperCase(); },
166
+
167
+ // Swatch Internet time; 000..999
168
+ B: function () {
169
+ var unixTime = jsdate.getTime() / 1000;
170
+ var secondsPassedToday = unixTime % 86400 + 3600; // since it's based off of UTC+1
171
+ if (secondsPassedToday < 0) { secondsPassedToday += 86400; }
172
+ var beats = ((secondsPassedToday) / 86.4) % 1000;
173
+ if (unixTime < 0) {
174
+ return Math.ceil(beats);
175
+ }
176
+ return Math.floor(beats);
177
+ },
178
+
179
+ // 12-Hours; 1..12
180
+ g: function () { return f.G() % 12 || 12; },
181
+
182
+ // 24-Hours; 0..23
183
+ G: function () { return jsdate.getHours(); },
184
+
185
+ // 12-Hours w/leading 0; 01..12
186
+ h: function () { return humanize.pad(f.g(), 2, '0'); },
187
+
188
+ // 24-Hours w/leading 0; 00..23
189
+ H: function () { return humanize.pad(f.G(), 2, '0'); },
190
+
191
+ // Minutes w/leading 0; 00..59
192
+ i: function () { return humanize.pad(jsdate.getMinutes(), 2, '0'); },
193
+
194
+ // Seconds w/leading 0; 00..59
195
+ s: function () { return humanize.pad(jsdate.getSeconds(), 2, '0'); },
196
+
197
+ // Microseconds; 000000-999000
198
+ u: function () { return humanize.pad(jsdate.getMilliseconds() * 1000, 6, '0'); },
199
+
200
+ // Whether or not the date is in daylight savings time
201
+ /*
202
+ I: function () {
203
+ // Compares Jan 1 minus Jan 1 UTC to Jul 1 minus Jul 1 UTC.
204
+ // If they are not equal, then DST is observed.
205
+ var Y = f.Y();
206
+ return 0 + ((new Date(Y, 0) - Date.UTC(Y, 0)) !== (new Date(Y, 6) - Date.UTC(Y, 6)));
207
+ },
208
+ */
209
+
210
+ // Difference to GMT in hour format; e.g. +0200
211
+ O: function () {
212
+ var tzo = jsdate.getTimezoneOffset();
213
+ var tzoNum = Math.abs(tzo);
214
+ return (tzo > 0 ? '-' : '+') + humanize.pad(Math.floor(tzoNum / 60) * 100 + tzoNum % 60, 4, '0');
215
+ },
216
+
217
+ // Difference to GMT w/colon; e.g. +02:00
218
+ P: function () {
219
+ var O = f.O();
220
+ return (O.substr(0, 3) + ':' + O.substr(3, 2));
221
+ },
222
+
223
+ // Timezone offset in seconds (-43200..50400)
224
+ Z: function () { return -jsdate.getTimezoneOffset() * 60; },
225
+
226
+ // Full Date/Time, ISO-8601 date
227
+ c: function () { return 'Y-m-d\\TH:i:sP'.replace(formatChr, formatChrCb); },
228
+
229
+ // RFC 2822
230
+ r: function () { return 'D, d M Y H:i:s O'.replace(formatChr, formatChrCb); },
231
+
232
+ // Seconds since UNIX epoch
233
+ U: function () { return jsdate.getTime() / 1000 || 0; }
234
+ };
235
+
236
+ return format.replace(formatChr, formatChrCb);
237
+ };
238
+
239
+
240
+ /**
241
+ * format number by adding thousands separaters and significant digits while rounding
242
+ */
243
+ humanize.numberFormat = function(number, decimals, decPoint, thousandsSep) {
244
+ decimals = isNaN(decimals) ? 2 : Math.abs(decimals);
245
+ decPoint = (decPoint === undefined) ? '.' : decPoint;
246
+ thousandsSep = (thousandsSep === undefined) ? ',' : thousandsSep;
247
+
248
+ var sign = number < 0 ? '-' : '';
249
+ number = Math.abs(+number || 0);
250
+
251
+ var intPart = parseInt(number.toFixed(decimals), 10) + '';
252
+ var j = intPart.length > 3 ? intPart.length % 3 : 0;
253
+
254
+ return sign + (j ? intPart.substr(0, j) + thousandsSep : '') + intPart.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep) + (decimals ? decPoint + Math.abs(number - intPart).toFixed(decimals).slice(2) : '');
255
+ };
256
+
257
+
258
+ /**
259
+ * For dates that are the current day or within one day, return 'today', 'tomorrow' or 'yesterday', as appropriate.
260
+ * Otherwise, format the date using the passed in format string.
261
+ *
262
+ * Examples (when 'today' is 17 Feb 2007):
263
+ * 16 Feb 2007 becomes yesterday.
264
+ * 17 Feb 2007 becomes today.
265
+ * 18 Feb 2007 becomes tomorrow.
266
+ * Any other day is formatted according to given argument or the DATE_FORMAT setting if no argument is given.
267
+ */
268
+ humanize.naturalDay = function(timestamp, format) {
269
+ timestamp = (timestamp === undefined) ? humanize.time() : timestamp;
270
+ format = (format === undefined) ? 'Y-m-d' : format;
271
+
272
+ var oneDay = 86400;
273
+ var d = new Date();
274
+ var today = (new Date(d.getFullYear(), d.getMonth(), d.getDate())).getTime() / 1000;
275
+
276
+ if (timestamp < today && timestamp >= today - oneDay) {
277
+ return 'yesterday';
278
+ } else if (timestamp >= today && timestamp < today + oneDay) {
279
+ return 'today';
280
+ } else if (timestamp >= today + oneDay && timestamp < today + 2 * oneDay) {
281
+ return 'tomorrow';
282
+ }
283
+
284
+ return humanize.date(format, timestamp);
285
+ };
286
+
287
+ /**
288
+ * returns a string representing how many seconds, minutes or hours ago it was or will be in the future
289
+ * Will always return a relative time, most granular of seconds to least granular of years. See unit tests for more details
290
+ */
291
+ humanize.relativeTime = function(timestamp) {
292
+ timestamp = (timestamp === undefined) ? humanize.time() : timestamp;
293
+
294
+ var currTime = humanize.time();
295
+ var timeDiff = currTime - timestamp;
296
+
297
+ // within 2 seconds
298
+ if (timeDiff < 2 && timeDiff > -2) {
299
+ return (timeDiff >= 0 ? 'just ' : '') + 'now';
300
+ }
301
+
302
+ // within a minute
303
+ if (timeDiff < 60 && timeDiff > -60) {
304
+ return (timeDiff >= 0 ? Math.floor(timeDiff) + ' seconds ago' : 'in ' + Math.floor(-timeDiff) + ' seconds');
305
+ }
306
+
307
+ // within 2 minutes
308
+ if (timeDiff < 120 && timeDiff > -120) {
309
+ return (timeDiff >= 0 ? 'about a minute ago' : 'in about a minute');
310
+ }
311
+
312
+ // within an hour
313
+ if (timeDiff < 3600 && timeDiff > -3600) {
314
+ return (timeDiff >= 0 ? Math.floor(timeDiff / 60) + ' minutes ago' : 'in ' + Math.floor(-timeDiff / 60) + ' minutes');
315
+ }
316
+
317
+ // within 2 hours
318
+ if (timeDiff < 7200 && timeDiff > -7200) {
319
+ return (timeDiff >= 0 ? 'about an hour ago' : 'in about an hour');
320
+ }
321
+
322
+ // within 24 hours
323
+ if (timeDiff < 86400 && timeDiff > -86400) {
324
+ return (timeDiff >= 0 ? Math.floor(timeDiff / 3600) + ' hours ago' : 'in ' + Math.floor(-timeDiff / 3600) + ' hours');
325
+ }
326
+
327
+ // within 2 days
328
+ var days2 = 2 * 86400;
329
+ if (timeDiff < days2 && timeDiff > -days2) {
330
+ return (timeDiff >= 0 ? '1 day ago' : 'in 1 day');
331
+ }
332
+
333
+ // within 29 days
334
+ var days29 = 29 * 86400;
335
+ if (timeDiff < days29 && timeDiff > -days29) {
336
+ return (timeDiff >= 0 ? Math.floor(timeDiff / 86400) + ' days ago' : 'in ' + Math.floor(-timeDiff / 86400) + ' days');
337
+ }
338
+
339
+ // within 60 days
340
+ var days60 = 60 * 86400;
341
+ if (timeDiff < days60 && timeDiff > -days60) {
342
+ return (timeDiff >= 0 ? 'about a month ago' : 'in about a month');
343
+ }
344
+
345
+ var currTimeYears = parseInt(humanize.date('Y', currTime), 10);
346
+ var timestampYears = parseInt(humanize.date('Y', timestamp), 10);
347
+ var currTimeMonths = currTimeYears * 12 + parseInt(humanize.date('n', currTime), 10);
348
+ var timestampMonths = timestampYears * 12 + parseInt(humanize.date('n', timestamp), 10);
349
+
350
+ // within a year
351
+ var monthDiff = currTimeMonths - timestampMonths;
352
+ if (monthDiff < 12 && monthDiff > -12) {
353
+ return (monthDiff >= 0 ? monthDiff + ' months ago' : 'in ' + (-monthDiff) + ' months');
354
+ }
355
+
356
+ var yearDiff = currTimeYears - timestampYears;
357
+ if (yearDiff < 2 && yearDiff > -2) {
358
+ return (yearDiff >= 0 ? 'a year ago' : 'in a year');
359
+ }
360
+
361
+ return (yearDiff >= 0 ? yearDiff + ' years ago' : 'in ' + (-yearDiff) + ' years');
362
+ };
363
+
364
+ /**
365
+ * Converts an integer to its ordinal as a string.
366
+ *
367
+ * 1 becomes 1st
368
+ * 2 becomes 2nd
369
+ * 3 becomes 3rd etc
370
+ */
371
+ humanize.ordinal = function(number) {
372
+ number = parseInt(number, 10);
373
+ number = isNaN(number) ? 0 : number;
374
+ var sign = number < 0 ? '-' : '';
375
+ number = Math.abs(number);
376
+ var tens = number % 100;
377
+
378
+ return sign + number + (tens > 4 && tens < 21 ? 'th' : {1: 'st', 2: 'nd', 3: 'rd'}[number % 10] || 'th');
379
+ };
380
+
381
+ /**
382
+ * Formats the value like a 'human-readable' file size (i.e. '13 KB', '4.1 MB', '102 bytes', etc).
383
+ *
384
+ * For example:
385
+ * If value is 123456789, the output would be 117.7 MB.
386
+ */
387
+ humanize.filesize = function(filesize, kilo, decimals, decPoint, thousandsSep, suffixSep) {
388
+ kilo = (kilo === undefined) ? 1024 : kilo;
389
+ if (filesize <= 0) { return '0 bytes'; }
390
+ if (filesize < kilo && decimals === undefined) { decimals = 0; }
391
+ if (suffixSep === undefined) { suffixSep = ' '; }
392
+ return humanize.intword(filesize, ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'], kilo, decimals, decPoint, thousandsSep, suffixSep);
393
+ };
394
+
395
+ /**
396
+ * Formats the value like a 'human-readable' number (i.e. '13 K', '4.1 M', '102', etc).
397
+ *
398
+ * For example:
399
+ * If value is 123456789, the output would be 117.7 M.
400
+ */
401
+ humanize.intword = function(number, units, kilo, decimals, decPoint, thousandsSep, suffixSep) {
402
+ var humanized, unit;
403
+
404
+ units = units || ['', 'K', 'M', 'B', 'T'],
405
+ unit = units.length - 1,
406
+ kilo = kilo || 1000,
407
+ decimals = isNaN(decimals) ? 2 : Math.abs(decimals),
408
+ decPoint = decPoint || '.',
409
+ thousandsSep = thousandsSep || ',',
410
+ suffixSep = suffixSep || '';
411
+
412
+ for (var i=0; i < units.length; i++) {
413
+ if (number < Math.pow(kilo, i+1)) {
414
+ unit = i;
415
+ break;
416
+ }
417
+ }
418
+ humanized = number / Math.pow(kilo, unit);
419
+
420
+ var suffix = units[unit] ? suffixSep + units[unit] : '';
421
+ return humanize.numberFormat(humanized, decimals, decPoint, thousandsSep) + suffix;
422
+ };
423
+
424
+ /**
425
+ * Replaces line breaks in plain text with appropriate HTML
426
+ * A single newline becomes an HTML line break (<br />) and a new line followed by a blank line becomes a paragraph break (</p>).
427
+ *
428
+ * For example:
429
+ * If value is Joel\nis a\n\nslug, the output will be <p>Joel<br />is a</p><p>slug</p>
430
+ */
431
+ humanize.linebreaks = function(str) {
432
+ // remove beginning and ending newlines
433
+ str = str.replace(/^([\n|\r]*)/, '');
434
+ str = str.replace(/([\n|\r]*)$/, '');
435
+
436
+ // normalize all to \n
437
+ str = str.replace(/(\r\n|\n|\r)/g, "\n");
438
+
439
+ // any consecutive new lines more than 2 gets turned into p tags
440
+ str = str.replace(/(\n{2,})/g, '</p><p>');
441
+
442
+ // any that are singletons get turned into br
443
+ str = str.replace(/\n/g, '<br />');
444
+ return '<p>' + str + '</p>';
445
+ };
446
+
447
+ /**
448
+ * Converts all newlines in a piece of plain text to HTML line breaks (<br />).
449
+ */
450
+ humanize.nl2br = function(str) {
451
+ return str.replace(/(\r\n|\n|\r)/g, '<br />');
452
+ };
453
+
454
+ /**
455
+ * Truncates a string if it is longer than the specified number of characters.
456
+ * Truncated strings will end with a translatable ellipsis sequence ('…').
457
+ */
458
+ humanize.truncatechars = function(string, length) {
459
+ if (string.length <= length) { return string; }
460
+ return string.substr(0, length) + '…';
461
+ };
462
+
463
+ /**
464
+ * Truncates a string after a certain number of words.
465
+ * Newlines within the string will be removed.
466
+ */
467
+ humanize.truncatewords = function(string, numWords) {
468
+ var words = string.split(' ');
469
+ if (words.length < numWords) { return string; }
470
+ return words.slice(0, numWords).join(' ') + '…';
471
+ };
472
+
473
+ }).call(this);