master
Raw Download raw file
  1
  2  'use strict';
  3
  4  /***
  5   * @package String
  6   * @dependency core
  7   * @description String manupulation, escaping, encoding, truncation, and:conversion.
  8   *
  9   ***/
 10
 11  function getAcronym(word) {
 12    var inflector = string.Inflector;
 13    var word = inflector && inflector.acronyms[word];
 14    if(isString(word)) {
 15      return word;
 16    }
 17  }
 18
 19  function checkRepeatRange(num) {
 20    num = +num;
 21    if(num < 0 || num === Infinity) {
 22      throw new RangeError('Invalid number');
 23    }
 24    return num;
 25  }
 26
 27  function padString(num, padding) {
 28    return repeatString(isDefined(padding) ? padding : ' ', num);
 29  }
 30
 31  function truncateString(str, length, from, ellipsis, split) {
 32    var str1, str2, len1, len2;
 33    if(str.length <= length) {
 34      return str.toString();
 35    }
 36    ellipsis = isUndefined(ellipsis) ? '...' : ellipsis;
 37    switch(from) {
 38      case 'left':
 39        str2 = split ? truncateOnWord(str, length, true) : str.slice(str.length - length);
 40        return ellipsis + str2;
 41      case 'middle':
 42        len1 = ceil(length / 2);
 43        len2 = floor(length / 2);
 44        str1 = split ? truncateOnWord(str, len1) : str.slice(0, len1);
 45        str2 = split ? truncateOnWord(str, len2, true) : str.slice(str.length - len2);
 46        return str1 + ellipsis + str2;
 47      default:
 48        str1 = split ? truncateOnWord(str, length) : str.slice(0, length);
 49        return str1 + ellipsis;
 50    }
 51  }
 52
 53  function truncateOnWord(str, limit, fromLeft) {
 54    if(fromLeft) {
 55      return truncateOnWord(str.reverse(), limit).reverse();
 56    }
 57    var reg = regexp('(?=[' + getTrimmableCharacters() + '])');
 58    var words = str.split(reg);
 59    var count = 0;
 60    return words.filter(function(word) {
 61      count += word.length;
 62      return count <= limit;
 63    }).join('');
 64  }
 65
 66  function numberOrIndex(str, n, from) {
 67    if(isString(n)) {
 68      n = str.indexOf(n);
 69      if(n === -1) {
 70        n = from ? str.length : 0;
 71      }
 72    }
 73    return n;
 74  }
 75
 76  var btoa, atob;
 77
 78  function buildBase64(key) {
 79    if(globalContext.btoa) {
 80      btoa = globalContext.btoa;
 81      atob = globalContext.atob;
 82      return;
 83    }
 84    var base64reg = /[^A-Za-z0-9\+\/\=]/g;
 85    btoa = function(str) {
 86      var output = '';
 87      var chr1, chr2, chr3;
 88      var enc1, enc2, enc3, enc4;
 89      var i = 0;
 90      do {
 91        chr1 = str.charCodeAt(i++);
 92        chr2 = str.charCodeAt(i++);
 93        chr3 = str.charCodeAt(i++);
 94        enc1 = chr1 >> 2;
 95        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
 96        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
 97        enc4 = chr3 & 63;
 98        if (isNaN(chr2)) {
 99          enc3 = enc4 = 64;
100        } else if (isNaN(chr3)) {
101          enc4 = 64;
102        }
103        output = output + key.charAt(enc1) + key.charAt(enc2) + key.charAt(enc3) + key.charAt(enc4);
104        chr1 = chr2 = chr3 = '';
105        enc1 = enc2 = enc3 = enc4 = '';
106      } while (i < str.length);
107      return output;
108    }
109    atob = function(input) {
110      var output = '';
111      var chr1, chr2, chr3;
112      var enc1, enc2, enc3, enc4;
113      var i = 0;
114      if(input.match(base64reg)) {
115        throw new Error('String contains invalid base64 characters');
116      }
117      input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
118      do {
119        enc1 = key.indexOf(input.charAt(i++));
120        enc2 = key.indexOf(input.charAt(i++));
121        enc3 = key.indexOf(input.charAt(i++));
122        enc4 = key.indexOf(input.charAt(i++));
123        chr1 = (enc1 << 2) | (enc2 >> 4);
124        chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
125        chr3 = ((enc3 & 3) << 6) | enc4;
126        output = output + chr(chr1);
127        if (enc3 != 64) {
128          output = output + chr(chr2);
129        }
130        if (enc4 != 64) {
131          output = output + chr(chr3);
132        }
133        chr1 = chr2 = chr3 = '';
134        enc1 = enc2 = enc3 = enc4 = '';
135      } while (i < input.length);
136      return output;
137    }
138  }
139
140  extend(string, true, false, {
141    /***
142     * @method repeat([num] = 0)
143     * @returns String
144     * @short Returns the string repeated [num] times.
145     * @example
146     *
147     *   'jumpy'.repeat(2) -> 'jumpyjumpy'
148     *   'a'.repeat(5)     -> 'aaaaa'
149     *   'a'.repeat(0)     -> ''
150     *
151     ***/
152    'repeat': function(num) {
153      num = checkRepeatRange(num);
154      return repeatString(this, num);
155    }
156
157  });
158
159  extend(string, true, function(reg) { return isRegExp(reg) || arguments.length > 2; }, {
160
161    /***
162     * @method startsWith(<find>, [pos] = 0, [case] = true)
163     * @returns Boolean
164     * @short Returns true if the string starts with <find>.
165     * @extra <find> may be either a string or regex. Search begins at [pos], which defaults to the entire string. Case sensitive if [case] is true.
166     * @example
167     *
168     *   'hello'.startsWith('hell')           -> true
169     *   'hello'.startsWith(/[a-h]/)          -> true
170     *   'hello'.startsWith('HELL')           -> false
171     *   'hello'.startsWith('ell', 1)         -> true
172     *   'hello'.startsWith('HELL', 0, false) -> true
173     *
174     ***/
175    'startsWith': function(reg) {
176      var args = arguments, pos = args[1], c = args[2], str = this, source;
177      if(pos) str = str.slice(pos);
178      if(isUndefined(c)) c = true;
179      source = isRegExp(reg) ? reg.source.replace('^', '') : escapeRegExp(reg);
180      return regexp('^' + source, c ? '' : 'i').test(str);
181    },
182
183    /***
184     * @method endsWith(<find>, [pos] = length, [case] = true)
185     * @returns Boolean
186     * @short Returns true if the string ends with <find>.
187     * @extra <find> may be either a string or regex. Search ends at [pos], which defaults to the entire string. Case sensitive if [case] is true.
188     * @example
189     *
190     *   'jumpy'.endsWith('py')            -> true
191     *   'jumpy'.endsWith(/[q-z]/)         -> true
192     *   'jumpy'.endsWith('MPY')           -> false
193     *   'jumpy'.endsWith('mp', 4)         -> false
194     *   'jumpy'.endsWith('MPY', 5, false) -> true
195     *
196     ***/
197    'endsWith': function(reg) {
198      var args = arguments, pos = args[1], c = args[2], str = this, source;
199      if(isDefined(pos)) str = str.slice(0, pos);
200      if(isUndefined(c)) c = true;
201      source = isRegExp(reg) ? reg.source.replace('$', '') : escapeRegExp(reg);
202      return regexp(source + '$', c ? '' : 'i').test(str);
203    }
204
205  });
206
207  extend(string, true, true, {
208
209     /***
210      * @method escapeRegExp()
211      * @returns String
212      * @short Escapes all RegExp tokens in the string.
213      * @example
214      *
215      *   'really?'.escapeRegExp()       -> 'really\?'
216      *   'yes.'.escapeRegExp()         -> 'yes\.'
217      *   '(not really)'.escapeRegExp() -> '\(not really\)'
218      *
219      ***/
220    'escapeRegExp': function() {
221      return escapeRegExp(this);
222    },
223
224     /***
225      * @method escapeURL([param] = false)
226      * @returns String
227      * @short Escapes characters in a string to make a valid URL.
228      * @extra If [param] is true, it will also escape valid URL characters for use as a URL parameter.
229      * @example
230      *
231      *   'http://foo.com/"bar"'.escapeURL()     -> 'http://foo.com/%22bar%22'
232      *   'http://foo.com/"bar"'.escapeURL(true) -> 'http%3A%2F%2Ffoo.com%2F%22bar%22'
233      *
234      ***/
235    'escapeURL': function(param) {
236      return param ? encodeURIComponent(this) : encodeURI(this);
237    },
238
239     /***
240      * @method unescapeURL([partial] = false)
241      * @returns String
242      * @short Restores escaped characters in a URL escaped string.
243      * @extra If [partial] is true, it will only unescape non-valid URL characters. [partial] is included here for completeness, but should very rarely be needed.
244      * @example
245      *
246      *   'http%3A%2F%2Ffoo.com%2Fthe%20bar'.unescapeURL()     -> 'http://foo.com/the bar'
247      *   'http%3A%2F%2Ffoo.com%2Fthe%20bar'.unescapeURL(true) -> 'http%3A%2F%2Ffoo.com%2Fthe bar'
248      *
249      ***/
250    'unescapeURL': function(param) {
251      return param ? decodeURI(this) : decodeURIComponent(this);
252    },
253
254     /***
255      * @method escapeHTML()
256      * @returns String
257      * @short Converts HTML characters to their entity equivalents.
258      * @example
259      *
260      *   '<p>some text</p>'.escapeHTML() -> '&lt;p&gt;some text&lt;/p&gt;'
261      *   'one & two'.escapeHTML()        -> 'one &amp; two'
262      *
263      ***/
264    'escapeHTML': function() {
265      return this.replace(/&/g,  '&amp;' )
266                 .replace(/</g,  '&lt;'  )
267                 .replace(/>/g,  '&gt;'  )
268                 .replace(/"/g,  '&quot;')
269                 .replace(/'/g,  '&apos;')
270                 .replace(/\//g, '&#x2f;');
271    },
272
273     /***
274      * @method unescapeHTML([partial] = false)
275      * @returns String
276      * @short Restores escaped HTML characters.
277      * @example
278      *
279      *   '&lt;p&gt;some text&lt;/p&gt;'.unescapeHTML() -> '<p>some text</p>'
280      *   'one &amp; two'.unescapeHTML()                -> 'one & two'
281      *
282      ***/
283    'unescapeHTML': function() {
284      return this.replace(/&lt;/g,   '<')
285                 .replace(/&gt;/g,   '>')
286                 .replace(/&quot;/g, '"')
287                 .replace(/&apos;/g, "'")
288                 .replace(/&#x2f;/g, '/')
289                 .replace(/&amp;/g,  '&');
290    },
291
292     /***
293      * @method encodeBase64()
294      * @returns String
295      * @short Encodes the string into base64 encoding.
296      * @extra This method wraps the browser native %btoa% when available, and uses a custom implementation when not available. It can also handle Unicode string encodings.
297      * @example
298      *
299      *   'gonna get encoded!'.encodeBase64()  -> 'Z29ubmEgZ2V0IGVuY29kZWQh'
300      *   'http://twitter.com/'.encodeBase64() -> 'aHR0cDovL3R3aXR0ZXIuY29tLw=='
301      *
302      ***/
303    'encodeBase64': function() {
304      return btoa(unescape(encodeURIComponent(this)));
305    },
306
307     /***
308      * @method decodeBase64()
309      * @returns String
310      * @short Decodes the string from base64 encoding.
311      * @extra This method wraps the browser native %atob% when available, and uses a custom implementation when not available. It can also handle Unicode string encodings.
312      * @example
313      *
314      *   'aHR0cDovL3R3aXR0ZXIuY29tLw=='.decodeBase64() -> 'http://twitter.com/'
315      *   'anVzdCBnb3QgZGVjb2RlZA=='.decodeBase64()     -> 'just got decoded!'
316      *
317      ***/
318    'decodeBase64': function() {
319      return decodeURIComponent(escape(atob(this)));
320    },
321
322    /***
323     * @method each([search] = single character, [fn])
324     * @returns Array
325     * @short Runs callback [fn] against each occurence of [search].
326     * @extra Returns an array of matches. [search] may be either a string or regex, and defaults to every character in the string.
327     * @example
328     *
329     *   'jumpy'.each() -> ['j','u','m','p','y']
330     *   'jumpy'.each(/[r-z]/) -> ['u','y']
331     *   'jumpy'.each(/[r-z]/, function(m) {
332     *     // Called twice: "u", "y"
333     *   });
334     *
335     ***/
336    'each': function(search, fn) {
337      var match, i, len;
338      if(isFunction(search)) {
339        fn = search;
340        search = /[\s\S]/g;
341      } else if(!search) {
342        search = /[\s\S]/g
343      } else if(isString(search)) {
344        search = regexp(escapeRegExp(search), 'gi');
345      } else if(isRegExp(search)) {
346        search = regexp(search.source, getRegExpFlags(search, 'g'));
347      }
348      match = this.match(search) || [];
349      if(fn) {
350        for(i = 0, len = match.length; i < len; i++) {
351          match[i] = fn.call(this, match[i], i, match) || match[i];
352        }
353      }
354      return match;
355    },
356
357    /***
358     * @method shift(<n>)
359     * @returns Array
360     * @short Shifts each character in the string <n> places in the character map.
361     * @example
362     *
363     *   'a'.shift(1)  -> 'b'
364     *   'ク'.shift(1) -> 'グ'
365     *
366     ***/
367    'shift': function(n) {
368      var result = '';
369      n = n || 0;
370      this.codes(function(c) {
371        result += chr(c + n);
372      });
373      return result;
374    },
375
376    /***
377     * @method codes([fn])
378     * @returns Array
379     * @short Runs callback [fn] against each character code in the string. Returns an array of character codes.
380     * @example
381     *
382     *   'jumpy'.codes() -> [106,117,109,112,121]
383     *   'jumpy'.codes(function(c) {
384     *     // Called 5 times: 106, 117, 109, 112, 121
385     *   });
386     *
387     ***/
388    'codes': function(fn) {
389      var codes = [], i, len;
390      for(i = 0, len = this.length; i < len; i++) {
391        var code = this.charCodeAt(i);
392        codes.push(code);
393        if(fn) fn.call(this, code, i);
394      }
395      return codes;
396    },
397
398    /***
399     * @method chars([fn])
400     * @returns Array
401     * @short Runs callback [fn] against each character in the string. Returns an array of characters.
402     * @example
403     *
404     *   'jumpy'.chars() -> ['j','u','m','p','y']
405     *   'jumpy'.chars(function(c) {
406     *     // Called 5 times: "j","u","m","p","y"
407     *   });
408     *
409     ***/
410    'chars': function(fn) {
411      return this.each(fn);
412    },
413
414    /***
415     * @method words([fn])
416     * @returns Array
417     * @short Runs callback [fn] against each word in the string. Returns an array of words.
418     * @extra A "word" here is defined as any sequence of non-whitespace characters.
419     * @example
420     *
421     *   'broken wear'.words() -> ['broken','wear']
422     *   'broken wear'.words(function(w) {
423     *     // Called twice: "broken", "wear"
424     *   });
425     *
426     ***/
427    'words': function(fn) {
428      return this.trim().each(/\S+/g, fn);
429    },
430
431    /***
432     * @method lines([fn])
433     * @returns Array
434     * @short Runs callback [fn] against each line in the string. Returns an array of lines.
435     * @example
436     *
437     *   'broken wear\nand\njumpy jump'.lines() -> ['broken wear','and','jumpy jump']
438     *   'broken wear\nand\njumpy jump'.lines(function(l) {
439     *     // Called three times: "broken wear", "and", "jumpy jump"
440     *   });
441     *
442     ***/
443    'lines': function(fn) {
444      return this.trim().each(/^.*$/gm, fn);
445    },
446
447    /***
448     * @method paragraphs([fn])
449     * @returns Array
450     * @short Runs callback [fn] against each paragraph in the string. Returns an array of paragraphs.
451     * @extra A paragraph here is defined as a block of text bounded by two or more line breaks.
452     * @example
453     *
454     *   'Once upon a time.\n\nIn the land of oz...'.paragraphs() -> ['Once upon a time.','In the land of oz...']
455     *   'Once upon a time.\n\nIn the land of oz...'.paragraphs(function(p) {
456     *     // Called twice: "Once upon a time.", "In teh land of oz..."
457     *   });
458     *
459     ***/
460    'paragraphs': function(fn) {
461      var paragraphs = this.trim().split(/[\r\n]{2,}/);
462      paragraphs = paragraphs.map(function(p) {
463        if(fn) var s = fn.call(p);
464        return s ? s : p;
465      });
466      return paragraphs;
467    },
468
469    /***
470     * @method isBlank()
471     * @returns Boolean
472     * @short Returns true if the string has a length of 0 or contains only whitespace.
473     * @example
474     *
475     *   ''.isBlank()      -> true
476     *   '   '.isBlank()   -> true
477     *   'noway'.isBlank() -> false
478     *
479     ***/
480    'isBlank': function() {
481      return this.trim().length === 0;
482    },
483
484    /***
485     * @method has(<find>)
486     * @returns Boolean
487     * @short Returns true if the string matches <find>.
488     * @extra <find> may be a string or regex.
489     * @example
490     *
491     *   'jumpy'.has('py')     -> true
492     *   'broken'.has(/[a-n]/) -> true
493     *   'broken'.has(/[s-z]/) -> false
494     *
495     ***/
496    'has': function(find) {
497      return this.search(isRegExp(find) ? find : escapeRegExp(find)) !== -1;
498    },
499
500
501    /***
502     * @method add(<str>, [index] = length)
503     * @returns String
504     * @short Adds <str> at [index]. Negative values are also allowed.
505     * @extra %insert% is provided as an alias, and is generally more readable when using an index.
506     * @example
507     *
508     *   'schfifty'.add(' five')      -> schfifty five
509     *   'dopamine'.insert('e', 3)       -> dopeamine
510     *   'spelling eror'.insert('r', -3) -> spelling error
511     *
512     ***/
513    'add': function(str, index) {
514      index = isUndefined(index) ? this.length : index;
515      return this.slice(0, index) + str + this.slice(index);
516    },
517
518    /***
519     * @method remove(<f>)
520     * @returns String
521     * @short Removes any part of the string that matches <f>.
522     * @extra <f> can be a string or a regex.
523     * @example
524     *
525     *   'schfifty five'.remove('f')     -> 'schity ive'
526     *   'schfifty five'.remove(/[a-f]/g) -> 'shity iv'
527     *
528     ***/
529    'remove': function(f) {
530      return this.replace(f, '');
531    },
532
533    /***
534     * @method reverse()
535     * @returns String
536     * @short Reverses the string.
537     * @example
538     *
539     *   'jumpy'.reverse()        -> 'ypmuj'
540     *   'lucky charms'.reverse() -> 'smrahc ykcul'
541     *
542     ***/
543    'reverse': function() {
544      return this.split('').reverse().join('');
545    },
546
547    /***
548     * @method compact()
549     * @returns String
550     * @short Compacts all white space in the string to a single space and trims the ends.
551     * @example
552     *
553     *   'too \n much \n space'.compact() -> 'too much space'
554     *   'enough \n '.compact()           -> 'enought'
555     *
556     ***/
557    'compact': function() {
558      return this.trim().replace(/([\r\n\s ])+/g, function(match, whitespace){
559        return whitespace === ' ' ? whitespace : ' ';
560      });
561    },
562
563    /***
564     * @method at(<index>, [loop] = true)
565     * @returns String or Array
566     * @short Gets the character(s) at a given index.
567     * @extra When [loop] is true, overshooting the end of the string (or the beginning) will begin counting from the other end. As an alternate syntax, passing multiple indexes will get the characters at those indexes.
568     * @example
569     *
570     *   'jumpy'.at(0)               -> 'j'
571     *   'jumpy'.at(2)               -> 'm'
572     *   'jumpy'.at(5)               -> 'j'
573     *   'jumpy'.at(5, false)        -> ''
574     *   'jumpy'.at(-1)              -> 'y'
575     *   'lucky charms'.at(2,4,6,8) -> ['u','k','y',c']
576     *
577     ***/
578    'at': function() {
579      return getEntriesForIndexes(this, arguments, true);
580    },
581
582    /***
583     * @method from([index] = 0)
584     * @returns String
585     * @short Returns a section of the string starting from [index].
586     * @example
587     *
588     *   'lucky charms'.from()   -> 'lucky charms'
589     *   'lucky charms'.from(7)  -> 'harms'
590     *
591     ***/
592    'from': function(from) {
593      return this.slice(numberOrIndex(this, from, true));
594    },
595
596    /***
597     * @method to([index] = end)
598     * @returns String
599     * @short Returns a section of the string ending at [index].
600     * @example
601     *
602     *   'lucky charms'.to()   -> 'lucky charms'
603     *   'lucky charms'.to(7)  -> 'lucky ch'
604     *
605     ***/
606    'to': function(to) {
607      if(isUndefined(to)) to = this.length;
608      return this.slice(0, numberOrIndex(this, to));
609    },
610
611    /***
612     * @method dasherize()
613     * @returns String
614     * @short Converts underscores and camel casing to hypens.
615     * @example
616     *
617     *   'a_farewell_to_arms'.dasherize() -> 'a-farewell-to-arms'
618     *   'capsLock'.dasherize()           -> 'caps-lock'
619     *
620     ***/
621    'dasherize': function() {
622      return this.underscore().replace(/_/g, '-');
623    },
624
625    /***
626     * @method underscore()
627     * @returns String
628     * @short Converts hyphens and camel casing to underscores.
629     * @example
630     *
631     *   'a-farewell-to-arms'.underscore() -> 'a_farewell_to_arms'
632     *   'capsLock'.underscore()           -> 'caps_lock'
633     *
634     ***/
635    'underscore': function() {
636      return this
637        .replace(/[-\s]+/g, '_')
638        .replace(string.Inflector && string.Inflector.acronymRegExp, function(acronym, index) {
639          return (index > 0 ? '_' : '') + acronym.toLowerCase();
640        })
641        .replace(/([A-Z\d]+)([A-Z][a-z])/g,'$1_$2')
642        .replace(/([a-z\d])([A-Z])/g,'$1_$2')
643        .toLowerCase();
644    },
645
646    /***
647     * @method camelize([first] = true)
648     * @returns String
649     * @short Converts underscores and hyphens to camel case. If [first] is true the first letter will also be capitalized.
650     * @extra If the Inflections package is included acryonyms can also be defined that will be used when camelizing.
651     * @example
652     *
653     *   'caps_lock'.camelize()              -> 'CapsLock'
654     *   'moz-border-radius'.camelize()      -> 'MozBorderRadius'
655     *   'moz-border-radius'.camelize(false) -> 'mozBorderRadius'
656     *
657     ***/
658    'camelize': function(first) {
659      return this.underscore().replace(/(^|_)([^_]+)/g, function(match, pre, word, index) {
660        var acronym = getAcronym(word), capitalize = first !== false || index > 0;
661        if(acronym) return capitalize ? acronym : acronym.toLowerCase();
662        return capitalize ? word.capitalize() : word;
663      });
664    },
665
666    /***
667     * @method spacify()
668     * @returns String
669     * @short Converts camel case, underscores, and hyphens to a properly spaced string.
670     * @example
671     *
672     *   'camelCase'.spacify()                         -> 'camel case'
673     *   'an-ugly-string'.spacify()                    -> 'an ugly string'
674     *   'oh-no_youDid-not'.spacify().capitalize(true) -> 'something else'
675     *
676     ***/
677    'spacify': function() {
678      return this.underscore().replace(/_/g, ' ');
679    },
680
681    /***
682     * @method stripTags([tag1], [tag2], ...)
683     * @returns String
684     * @short Strips all HTML tags from the string.
685     * @extra Tags to strip may be enumerated in the parameters, otherwise will strip all.
686     * @example
687     *
688     *   '<p>just <b>some</b> text</p>'.stripTags()    -> 'just some text'
689     *   '<p>just <b>some</b> text</p>'.stripTags('p') -> 'just <b>some</b> text'
690     *
691     ***/
692    'stripTags': function() {
693      var str = this, args = arguments.length > 0 ? arguments : [''];
694      flattenedArgs(args, function(tag) {
695        str = str.replace(regexp('<\/?' + escapeRegExp(tag) + '[^<>]*>', 'gi'), '');
696      });
697      return str;
698    },
699
700    /***
701     * @method removeTags([tag1], [tag2], ...)
702     * @returns String
703     * @short Removes all HTML tags and their contents from the string.
704     * @extra Tags to remove may be enumerated in the parameters, otherwise will remove all.
705     * @example
706     *
707     *   '<p>just <b>some</b> text</p>'.removeTags()    -> ''
708     *   '<p>just <b>some</b> text</p>'.removeTags('b') -> '<p>just text</p>'
709     *
710     ***/
711    'removeTags': function() {
712      var str = this, args = arguments.length > 0 ? arguments : ['\\S+'];
713      flattenedArgs(args, function(t) {
714        var reg = regexp('<(' + t + ')[^<>]*(?:\\/>|>.*?<\\/\\1>)', 'gi');
715        str = str.replace(reg, '');
716      });
717      return str;
718    },
719
720    /***
721     * @method truncate(<length>, [from] = 'right', [ellipsis] = '...')
722     * @returns String
723     * @short Truncates a string.
724     * @extra [from] can be %'right'%, %'left'%, or %'middle'%. If the string is shorter than <length>, [ellipsis] will not be added.
725     * @example
726     *
727     *   'sittin on the dock of the bay'.truncate(18)           -> 'just sittin on the do...'
728     *   'sittin on the dock of the bay'.truncate(18, 'left')   -> '...the dock of the bay'
729     *   'sittin on the dock of the bay'.truncate(18, 'middle') -> 'just sitt...of the bay'
730     *
731     ***/
732    'truncate': function(length, from, ellipsis) {
733      return truncateString(this, length, from, ellipsis);
734    },
735
736    /***
737     * @method truncateOnWord(<length>, [from] = 'right', [ellipsis] = '...')
738     * @returns String
739     * @short Truncates a string without splitting up words.
740     * @extra [from] can be %'right'%, %'left'%, or %'middle'%. If the string is shorter than <length>, [ellipsis] will not be added.
741     * @example
742     *
743     *   'here we go'.truncateOnWord(5)               -> 'here...'
744     *   'here we go'.truncateOnWord(5, 'left')       -> '...we go'
745     *
746     ***/
747    'truncateOnWord': function(length, from, ellipsis) {
748      return truncateString(this, length, from, ellipsis, true);
749    },
750
751    /***
752     * @method pad[Side](<num> = null, [padding] = ' ')
753     * @returns String
754     * @short Pads the string out with [padding] to be exactly <num> characters.
755     *
756     * @set
757     *   pad
758     *   padLeft
759     *   padRight
760     *
761     * @example
762     *
763     *   'wasabi'.pad(8)           -> ' wasabi '
764     *   'wasabi'.padLeft(8)       -> '  wasabi'
765     *   'wasabi'.padRight(8)      -> 'wasabi  '
766     *   'wasabi'.padRight(8, '-') -> 'wasabi--'
767     *
768     ***/
769    'pad': function(num, padding) {
770      var half, front, back;
771      num   = checkRepeatRange(num);
772      half  = max(0, num - this.length) / 2;
773      front = floor(half);
774      back  = ceil(half);
775      return padString(front, padding) + this + padString(back, padding);
776    },
777
778    'padLeft': function(num, padding) {
779      num = checkRepeatRange(num);
780      return padString(max(0, num - this.length), padding) + this;
781    },
782
783    'padRight': function(num, padding) {
784      num = checkRepeatRange(num);
785      return this + padString(max(0, num - this.length), padding);
786    },
787
788    /***
789     * @method first([n] = 1)
790     * @returns String
791     * @short Returns the first [n] characters of the string.
792     * @example
793     *
794     *   'lucky charms'.first()   -> 'l'
795     *   'lucky charms'.first(3)  -> 'luc'
796     *
797     ***/
798    'first': function(num) {
799      if(isUndefined(num)) num = 1;
800      return this.substr(0, num);
801    },
802
803    /***
804     * @method last([n] = 1)
805     * @returns String
806     * @short Returns the last [n] characters of the string.
807     * @example
808     *
809     *   'lucky charms'.last()   -> 's'
810     *   'lucky charms'.last(3)  -> 'rms'
811     *
812     ***/
813    'last': function(num) {
814      if(isUndefined(num)) num = 1;
815      var start = this.length - num < 0 ? 0 : this.length - num;
816      return this.substr(start);
817    },
818
819    /***
820     * @method toNumber([base] = 10)
821     * @returns Number
822     * @short Converts the string into a number.
823     * @extra Any value with a "." fill be converted to a floating point value, otherwise an integer.
824     * @example
825     *
826     *   '153'.toNumber()    -> 153
827     *   '12,000'.toNumber() -> 12000
828     *   '10px'.toNumber()   -> 10
829     *   'ff'.toNumber(16)   -> 255
830     *
831     ***/
832    'toNumber': function(base) {
833      return stringToNumber(this, base);
834    },
835
836    /***
837     * @method capitalize([all] = false)
838     * @returns String
839     * @short Capitalizes the first character in the string and downcases all other letters.
840     * @extra If [all] is true, all words in the string will be capitalized.
841     * @example
842     *
843     *   'hello'.capitalize()           -> 'Hello'
844     *   'hello kitty'.capitalize()     -> 'Hello kitty'
845     *   'hello kitty'.capitalize(true) -> 'Hello Kitty'
846     *
847     *
848     ***/
849    'capitalize': function(all) {
850      var lastResponded;
851      return this.toLowerCase().replace(all ? /[^']/g : /^\S/, function(lower) {
852        var upper = lower.toUpperCase(), result;
853        result = lastResponded ? lower : upper;
854        lastResponded = upper !== lower;
855        return result;
856      });
857    },
858
859    /***
860     * @method assign(<obj1>, <obj2>, ...)
861     * @returns String
862     * @short Assigns variables to tokens in a string, demarcated with `{}`.
863     * @extra If an object is passed, it's properties can be assigned using the object's keys (i.e. {name}). If a non-object (string, number, etc.) is passed it can be accessed by the argument number beginning with {1} (as with regex tokens). Multiple objects can be passed and will be merged together (original objects are unaffected).
864     * @example
865     *
866     *   'Welcome, Mr. {name}.'.assign({ name: 'Franklin' })   -> 'Welcome, Mr. Franklin.'
867     *   'You are {1} years old today.'.assign(14)             -> 'You are 14 years old today.'
868     *   '{n} and {r}'.assign({ n: 'Cheech' }, { r: 'Chong' }) -> 'Cheech and Chong'
869     *
870     ***/
871    'assign': function() {
872      var assign = {};
873      flattenedArgs(arguments, function(a, i) {
874        if(isObjectType(a)) {
875          simpleMerge(assign, a);
876        } else {
877          assign[i + 1] = a;
878        }
879      });
880      return this.replace(/\{([^{]+?)\}/g, function(m, key) {
881        return hasOwnProperty(assign, key) ? assign[key] : m;
882      });
883    }
884
885  });
886
887
888  // Aliases
889
890  extend(string, true, true, {
891
892    /***
893     * @method insert()
894     * @alias add
895     *
896     ***/
897    'insert': string.prototype.add
898  });
899
900  buildBase64('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=');
901