/***
* @package String
* @dependency core
* @description String manupulation, escaping, encoding, truncation, and:conversion.
*
***/
function getAcronym(word) {
var inflector = string.Inflector;
var word = inflector && inflector.acronyms[word];
if(isString(word)) {
return word;
}
}
function padString(str, p, left, right) {
var padding = string(p);
if(padding != p) {
padding = '';
}
if(!isNumber(left)) left = 1;
if(!isNumber(right)) right = 1;
return padding.repeat(left) + str + padding.repeat(right);
}
function chr(num) {
return string.fromCharCode(num);
}
var btoa, atob;
function buildBase64(key) {
if(this.btoa) {
btoa = this.btoa;
atob = this.atob;
return;
}
var base64reg = /[^A-Za-z0-9\+\/\=]/g;
btoa = function(str) {
var output = '';
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
do {
chr1 = str.charCodeAt(i++);
chr2 = str.charCodeAt(i++);
chr3 = str.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output + key.charAt(enc1) + key.charAt(enc2) + key.charAt(enc3) + key.charAt(enc4);
chr1 = chr2 = chr3 = '';
enc1 = enc2 = enc3 = enc4 = '';
} while (i < str.length);
return output;
}
atob = function(input) {
var output = '';
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
if(input.match(base64reg)) {
throw new Error('String contains invalid base64 characters');
}
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
do {
enc1 = key.indexOf(input.charAt(i++));
enc2 = key.indexOf(input.charAt(i++));
enc3 = key.indexOf(input.charAt(i++));
enc4 = key.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + chr(chr1);
if (enc3 != 64) {
output = output + chr(chr2);
}
if (enc4 != 64) {
output = output + chr(chr3);
}
chr1 = chr2 = chr3 = '';
enc1 = enc2 = enc3 = enc4 = '';
} while (i < input.length);
return output;
}
}
extend(string, true, false, {
/***
* @method escapeRegExp()
* @returns String
* @short Escapes all RegExp tokens in the string.
* @example
*
* 'really?'.escapeRegExp() -> 'really\?'
* 'yes.'.escapeRegExp() -> 'yes\.'
* '(not really)'.escapeRegExp() -> '\(not really\)'
*
***/
'escapeRegExp': function() {
return escapeRegExp(this);
},
/***
* @method escapeURL([param] = false)
* @returns String
* @short Escapes characters in a string to make a valid URL.
* @extra If [param] is true, it will also escape valid URL characters for use as a URL parameter.
* @example
*
* 'http://foo.com/"bar"'.escapeURL() -> 'http://foo.com/%22bar%22'
* 'http://foo.com/"bar"'.escapeURL(true) -> 'http%3A%2F%2Ffoo.com%2F%22bar%22'
*
***/
'escapeURL': function(param) {
return param ? encodeURIComponent(this) : encodeURI(this);
},
/***
* @method unescapeURL([partial] = false)
* @returns String
* @short Restores escaped characters in a URL escaped string.
* @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.
* @example
*
* 'http%3A%2F%2Ffoo.com%2Fthe%20bar'.unescapeURL() -> 'http://foo.com/the bar'
* 'http%3A%2F%2Ffoo.com%2Fthe%20bar'.unescapeURL(true) -> 'http%3A%2F%2Ffoo.com%2Fthe bar'
*
***/
'unescapeURL': function(param) {
return param ? decodeURI(this) : decodeURIComponent(this);
},
/***
* @method escapeHTML()
* @returns String
* @short Converts HTML characters to their entity equivalents.
* @example
*
* '
some text
'.escapeHTML() -> '<p>some text</p>'
* 'one & two'.escapeHTML() -> 'one & two'
*
***/
'escapeHTML': function() {
return this.replace(/&/g, '&').replace(//g, '>');
},
/***
* @method unescapeHTML([partial] = false)
* @returns String
* @short Restores escaped HTML characters.
* @example
*
* '<p>some text</p>'.unescapeHTML() -> 'some text
'
* 'one & two'.unescapeHTML() -> 'one & two'
*
***/
'unescapeHTML': function() {
return this.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
},
/***
* @method encodeBase64()
* @returns String
* @short Encodes the string into base64 encoding.
* @extra This method wraps the browser native %btoa% when available, and uses a custom implementation when not available.
* @example
*
* 'gonna get encoded!'.encodeBase64() -> 'Z29ubmEgZ2V0IGVuY29kZWQh'
* 'http://twitter.com/'.encodeBase64() -> 'aHR0cDovL3R3aXR0ZXIuY29tLw=='
*
***/
'encodeBase64': function() {
return btoa(this);
},
/***
* @method decodeBase64()
* @returns String
* @short Decodes the string from base64 encoding.
* @extra This method wraps the browser native %atob% when available, and uses a custom implementation when not available.
* @example
*
* 'aHR0cDovL3R3aXR0ZXIuY29tLw=='.decodeBase64() -> 'http://twitter.com/'
* 'anVzdCBnb3QgZGVjb2RlZA=='.decodeBase64() -> 'just got decoded!'
*
***/
'decodeBase64': function() {
return atob(this);
},
/***
* @method each([search] = single character, [fn])
* @returns Array
* @short Runs callback [fn] against each occurence of [search].
* @extra Returns an array of matches. [search] may be either a string or regex, and defaults to every character in the string.
* @example
*
* 'jumpy'.each() -> ['j','u','m','p','y']
* 'jumpy'.each(/[r-z]/) -> ['u','y']
* 'jumpy'.each(/[r-z]/, function(m) {
* // Called twice: "u", "y"
* });
*
***/
'each': function(search, fn) {
var match, i;
if(isFunction(search)) {
fn = search;
search = /[\s\S]/g;
} else if(!search) {
search = /[\s\S]/g
} else if(isString(search)) {
search = regexp(escapeRegExp(search), 'gi');
} else if(isRegExp(search)) {
search = regexp(search.source, getRegExpFlags(search, 'g'));
}
match = this.match(search) || [];
if(fn) {
for(i = 0; i < match.length; i++) {
match[i] = fn.call(this, match[i], i, match) || match[i];
}
}
return match;
},
/***
* @method shift()
* @returns Array
* @short Shifts each character in the string places in the character map.
* @example
*
* 'a'.shift(1) -> 'b'
* 'ク'.shift(1) -> 'グ'
*
***/
'shift': function(n) {
var result = '';
n = n || 0;
this.codes(function(c) {
result += chr(c + n);
});
return result;
},
/***
* @method codes([fn])
* @returns Array
* @short Runs callback [fn] against each character code in the string. Returns an array of character codes.
* @example
*
* 'jumpy'.codes() -> [106,117,109,112,121]
* 'jumpy'.codes(function(c) {
* // Called 5 times: 106, 117, 109, 112, 121
* });
*
***/
'codes': function(fn) {
var codes = [];
for(var i=0; i ['j','u','m','p','y']
* 'jumpy'.chars(function(c) {
* // Called 5 times: "j","u","m","p","y"
* });
*
***/
'chars': function(fn) {
return this.each(fn);
},
/***
* @method words([fn])
* @returns Array
* @short Runs callback [fn] against each word in the string. Returns an array of words.
* @extra A "word" here is defined as any sequence of non-whitespace characters.
* @example
*
* 'broken wear'.words() -> ['broken','wear']
* 'broken wear'.words(function(w) {
* // Called twice: "broken", "wear"
* });
*
***/
'words': function(fn) {
return this.trim().each(/\S+/g, fn);
},
/***
* @method lines([fn])
* @returns Array
* @short Runs callback [fn] against each line in the string. Returns an array of lines.
* @example
*
* 'broken wear\nand\njumpy jump'.lines() -> ['broken wear','and','jumpy jump']
* 'broken wear\nand\njumpy jump'.lines(function(l) {
* // Called three times: "broken wear", "and", "jumpy jump"
* });
*
***/
'lines': function(fn) {
return this.trim().each(/^.*$/gm, fn);
},
/***
* @method paragraphs([fn])
* @returns Array
* @short Runs callback [fn] against each paragraph in the string. Returns an array of paragraphs.
* @extra A paragraph here is defined as a block of text bounded by two or more line breaks.
* @example
*
* 'Once upon a time.\n\nIn the land of oz...'.paragraphs() -> ['Once upon a time.','In the land of oz...']
* 'Once upon a time.\n\nIn the land of oz...'.paragraphs(function(p) {
* // Called twice: "Once upon a time.", "In teh land of oz..."
* });
*
***/
'paragraphs': function(fn) {
var paragraphs = this.trim().split(/[\r\n]{2,}/);
paragraphs = paragraphs.map(function(p) {
if(fn) var s = fn.call(p);
return s ? s : p;
});
return paragraphs;
},
/***
* @method startsWith(, [case] = true)
* @returns Boolean
* @short Returns true if the string starts with .
* @extra may be either a string or regex. Case sensitive if [case] is true.
* @example
*
* 'hello'.startsWith('hell') -> true
* 'hello'.startsWith(/[a-h]/) -> true
* 'hello'.startsWith('HELL') -> false
* 'hello'.startsWith('HELL', false) -> true
*
***/
'startsWith': function(reg, c) {
if(isUndefined(c)) c = true;
var source = isRegExp(reg) ? reg.source.replace('^', '') : escapeRegExp(reg);
return regexp('^' + source, c ? '' : 'i').test(this);
},
/***
* @method endsWith(, [case] = true)
* @returns Boolean
* @short Returns true if the string ends with .
* @extra may be either a string or regex. Case sensitive if [case] is true.
* @example
*
* 'jumpy'.endsWith('py') -> true
* 'jumpy'.endsWith(/[q-z]/) -> true
* 'jumpy'.endsWith('MPY') -> false
* 'jumpy'.endsWith('MPY', false) -> true
*
***/
'endsWith': function(reg, c) {
if(isUndefined(c)) c = true;
var source = isRegExp(reg) ? reg.source.replace('$', '') : escapeRegExp(reg);
return regexp(source + '$', c ? '' : 'i').test(this);
},
/***
* @method isBlank()
* @returns Boolean
* @short Returns true if the string has a length of 0 or contains only whitespace.
* @example
*
* ''.isBlank() -> true
* ' '.isBlank() -> true
* 'noway'.isBlank() -> false
*
***/
'isBlank': function() {
return this.trim().length === 0;
},
/***
* @method has()
* @returns Boolean
* @short Returns true if the string matches .
* @extra may be a string or regex.
* @example
*
* 'jumpy'.has('py') -> true
* 'broken'.has(/[a-n]/) -> true
* 'broken'.has(/[s-z]/) -> false
*
***/
'has': function(find) {
return this.search(isRegExp(find) ? find : escapeRegExp(find)) !== -1;
},
/***
* @method add(, [index] = length)
* @returns String
* @short Adds at [index]. Negative values are also allowed.
* @extra %insert% is provided as an alias, and is generally more readable when using an index.
* @example
*
* 'schfifty'.add(' five') -> schfifty five
* 'dopamine'.insert('e', 3) -> dopeamine
* 'spelling eror'.insert('r', -3) -> spelling error
*
***/
'add': function(str, index) {
index = isUndefined(index) ? this.length : index;
return this.slice(0, index) + str + this.slice(index);
},
/***
* @method remove()
* @returns String
* @short Removes any part of the string that matches .
* @extra can be a string or a regex.
* @example
*
* 'schfifty five'.remove('f') -> 'schity ive'
* 'schfifty five'.remove(/[a-f]/g) -> 'shity iv'
*
***/
'remove': function(f) {
return this.replace(f, '');
},
/***
* @method reverse()
* @returns String
* @short Reverses the string.
* @example
*
* 'jumpy'.reverse() -> 'ypmuj'
* 'lucky charms'.reverse() -> 'smrahc ykcul'
*
***/
'reverse': function() {
return this.split('').reverse().join('');
},
/***
* @method compact()
* @returns String
* @short Compacts all white space in the string to a single space and trims the ends.
* @example
*
* 'too \n much \n space'.compact() -> 'too much space'
* 'enough \n '.compact() -> 'enought'
*
***/
'compact': function() {
return this.trim().replace(/([\r\n\s ])+/g, function(match, whitespace){
return whitespace === ' ' ? whitespace : ' ';
});
},
/***
* @method at(, [loop] = true)
* @returns String or Array
* @short Gets the character(s) at a given index.
* @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.
* @example
*
* 'jumpy'.at(0) -> 'j'
* 'jumpy'.at(2) -> 'm'
* 'jumpy'.at(5) -> 'j'
* 'jumpy'.at(5, false) -> ''
* 'jumpy'.at(-1) -> 'y'
* 'lucky charms'.at(2,4,6,8) -> ['u','k','y',c']
*
***/
'at': function() {
return entryAtIndex(this, arguments, true);
},
/***
* @method from([index] = 0)
* @returns String
* @short Returns a section of the string starting from [index].
* @example
*
* 'lucky charms'.from() -> 'lucky charms'
* 'lucky charms'.from(7) -> 'harms'
*
***/
'from': function(num) {
return this.slice(num);
},
/***
* @method to([index] = end)
* @returns String
* @short Returns a section of the string ending at [index].
* @example
*
* 'lucky charms'.to() -> 'lucky charms'
* 'lucky charms'.to(7) -> 'lucky ch'
*
***/
'to': function(num) {
if(isUndefined(num)) num = this.length;
return this.slice(0, num);
},
/***
* @method dasherize()
* @returns String
* @short Converts underscores and camel casing to hypens.
* @example
*
* 'a_farewell_to_arms'.dasherize() -> 'a-farewell-to-arms'
* 'capsLock'.dasherize() -> 'caps-lock'
*
***/
'dasherize': function() {
return this.underscore().replace(/_/g, '-');
},
/***
* @method underscore()
* @returns String
* @short Converts hyphens and camel casing to underscores.
* @example
*
* 'a-farewell-to-arms'.underscore() -> 'a_farewell_to_arms'
* 'capsLock'.underscore() -> 'caps_lock'
*
***/
'underscore': function() {
return this
.replace(/[-\s]+/g, '_')
.replace(string.Inflector && string.Inflector.acronymRegExp, function(acronym, index) {
return (index > 0 ? '_' : '') + acronym.toLowerCase();
})
.replace(/([A-Z\d]+)([A-Z][a-z])/g,'$1_$2')
.replace(/([a-z\d])([A-Z])/g,'$1_$2')
.toLowerCase();
},
/***
* @method camelize([first] = true)
* @returns String
* @short Converts underscores and hyphens to camel case. If [first] is true the first letter will also be capitalized.
* @extra If the Inflections package is included acryonyms can also be defined that will be used when camelizing.
* @example
*
* 'caps_lock'.camelize() -> 'CapsLock'
* 'moz-border-radius'.camelize() -> 'MozBorderRadius'
* 'moz-border-radius'.camelize(false) -> 'mozBorderRadius'
*
***/
'camelize': function(first) {
return this.underscore().replace(/(^|_)([^_]+)/g, function(match, pre, word, index) {
var acronym = getAcronym(word), capitalize = first !== false || index > 0;
if(acronym) return capitalize ? acronym : acronym.toLowerCase();
return capitalize ? word.capitalize() : word;
});
},
/***
* @method spacify()
* @returns String
* @short Converts camel case, underscores, and hyphens to a properly spaced string.
* @example
*
* 'camelCase'.spacify() -> 'camel case'
* 'an-ugly-string'.spacify() -> 'an ugly string'
* 'oh-no_youDid-not'.spacify().capitalize(true) -> 'something else'
*
***/
'spacify': function() {
return this.underscore().replace(/_/g, ' ');
},
/***
* @method stripTags([tag1], [tag2], ...)
* @returns String
* @short Strips all HTML tags from the string.
* @extra Tags to strip may be enumerated in the parameters, otherwise will strip all.
* @example
*
* 'just some text
'.stripTags() -> 'just some text'
* 'just some text
'.stripTags('p') -> 'just some text'
*
***/
'stripTags': function() {
var str = this, args = arguments.length > 0 ? arguments : [''];
multiArgs(args, function(tag) {
str = str.replace(regexp('<\/?' + escapeRegExp(tag) + '[^<>]*>', 'gi'), '');
});
return str;
},
/***
* @method removeTags([tag1], [tag2], ...)
* @returns String
* @short Removes all HTML tags and their contents from the string.
* @extra Tags to remove may be enumerated in the parameters, otherwise will remove all.
* @example
*
* 'just some text
'.removeTags() -> ''
* 'just some text
'.removeTags('b') -> 'just text
'
*
***/
'removeTags': function() {
var str = this, args = arguments.length > 0 ? arguments : ['\\S+'];
multiArgs(args, function(t) {
var reg = regexp('<(' + t + ')[^<>]*(?:\\/>|>.*?<\\/\\1>)', 'gi');
str = str.replace(reg, '');
});
return str;
},
/***
* @method truncate(, [split] = true, [from] = 'right', [ellipsis] = '...')
* @returns Object
* @short Truncates a string.
* @extra If [split] is %false%, will not split words up, and instead discard the word where the truncation occurred. [from] can also be %"middle"% or %"left"%.
* @example
*
* 'just sittin on the dock of the bay'.truncate(20) -> 'just sittin on the do...'
* 'just sittin on the dock of the bay'.truncate(20, false) -> 'just sittin on the...'
* 'just sittin on the dock of the bay'.truncate(20, true, 'middle') -> 'just sitt...of the bay'
* 'just sittin on the dock of the bay'.truncate(20, true, 'left') -> '...the dock of the bay'
*
***/
'truncate': function(length, split, from, ellipsis) {
var pos,
prepend = '',
append = '',
str = this.toString(),
chars = '[' + getTrimmableCharacters() + ']+',
space = '[^' + getTrimmableCharacters() + ']*',
reg = regexp(chars + space + '$');
ellipsis = isUndefined(ellipsis) ? '...' : string(ellipsis);
if(str.length <= length) {
return str;
}
switch(from) {
case 'left':
pos = str.length - length;
prepend = ellipsis;
str = str.slice(pos);
reg = regexp('^' + space + chars);
break;
case 'middle':
pos = floor(length / 2);
append = ellipsis + str.slice(str.length - pos).trimLeft();
str = str.slice(0, pos);
break;
default:
pos = length;
append = ellipsis;
str = str.slice(0, pos);
}
if(split === false && this.slice(pos, pos + 1).match(/\S/)) {
str = str.remove(reg);
}
return prepend + str + append;
},
/***
* @method pad[Side]( = '', [num] = 1)
* @returns String
* @short Pads either/both sides of the string.
* @extra [num] is the number of characters on each side, and [padding] is the character to pad with.
*
* @set
* pad
* padLeft
* padRight
*
* @example
*
* 'wasabi'.pad('-') -> '-wasabi-'
* 'wasabi'.pad('-', 2) -> '--wasabi--'
* 'wasabi'.padLeft('-', 2) -> '--wasabi'
* 'wasabi'.padRight('-', 2) -> 'wasabi--'
*
***/
'pad': function(padding, num) {
return repeatString(num, padding) + this + repeatString(num, padding);
},
'padLeft': function(padding, num) {
return repeatString(num, padding) + this;
},
'padRight': function(padding, num) {
return this + repeatString(num, padding);
},
/***
* @method first([n] = 1)
* @returns String
* @short Returns the first [n] characters of the string.
* @example
*
* 'lucky charms'.first() -> 'l'
* 'lucky charms'.first(3) -> 'luc'
*
***/
'first': function(num) {
if(isUndefined(num)) num = 1;
return this.substr(0, num);
},
/***
* @method last([n] = 1)
* @returns String
* @short Returns the last [n] characters of the string.
* @example
*
* 'lucky charms'.last() -> 's'
* 'lucky charms'.last(3) -> 'rms'
*
***/
'last': function(num) {
if(isUndefined(num)) num = 1;
var start = this.length - num < 0 ? 0 : this.length - num;
return this.substr(start);
},
/***
* @method repeat([num] = 0)
* @returns String
* @short Returns the string repeated [num] times.
* @example
*
* 'jumpy'.repeat(2) -> 'jumpyjumpy'
* 'a'.repeat(5) -> 'aaaaa'
*
***/
'repeat': function(num) {
var str = '', i = 0;
if(isNumber(num) && num > 0) {
while(i < num) {
str += this;
i++;
}
}
return str;
},
/***
* @method toNumber([base] = 10)
* @returns Number
* @short Converts the string into a number.
* @extra Any value with a "." fill be converted to a floating point value, otherwise an integer.
* @example
*
* '153'.toNumber() -> 153
* '12,000'.toNumber() -> 12000
* '10px'.toNumber() -> 10
* 'ff'.toNumber(16) -> 255
*
***/
'toNumber': function(base) {
var str = this.replace(/,/g, '');
return str.match(/\./) ? parseFloat(str) : parseInt(str, base || 10);
},
/***
* @method capitalize([all] = false)
* @returns String
* @short Capitalizes the first character in the string.
* @extra If [all] is true, all words in the string will be capitalized.
* @example
*
* 'hello'.capitalize() -> 'Hello'
* 'hello kitty'.capitalize() -> 'Hello kitty'
* 'hello kitty'.capitalize(true) -> 'Hello Kitty'
*
*
***/
'capitalize': function(all) {
var lastResponded;
return this.toLowerCase().replace(all ? /[\s\S]/g : /^\S/, function(lower) {
var upper = lower.toUpperCase(), result;
result = lastResponded ? lower : upper;
lastResponded = upper !== lower;
return result;
});
},
/***
* @method assign(, , ...)
* @returns String
* @short Assigns variables to tokens in a string.
* @extra If an object is passed, it's properties can be assigned using the object's keys. 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).
* @example
*
* 'Welcome, Mr. {name}.'.assign({ name: 'Franklin' }) -> 'Welcome, Mr. Franklin.'
* 'You are {1} years old today.'.assign(14) -> 'You are 14 years old today.'
* '{n} and {r}'.assign({ n: 'Cheech' }, { r: 'Chong' }) -> 'Cheech and Chong'
*
***/
'assign': function() {
var assign = {};
multiArgs(arguments, function(a, i) {
if(isObject(a)) {
simpleMerge(assign, a);
} else {
assign[i + 1] = a;
}
});
return this.replace(/\{([^{]+?)\}/g, function(m, key) {
return hasOwnProperty(assign, key) ? assign[key] : m;
});
},
/***
* @method namespace([init] = global)
* @returns Mixed
* @short Finds the namespace or property indicated by the string.
* @extra [init] can be passed to provide a starting context, otherwise the global context will be used. If any level returns a falsy value, that will be the final result.
* @example
*
* 'Path.To.Namespace'.namespace() -> Path.To.Namespace
* '$.fn'.namespace() -> $.fn
*
***/
'namespace': function(context) {
context = context || globalContext;
iterateOverObject(this.split('.'), function(i,s) {
return !!(context = context[s]);
});
return context;
}
});
// Aliases
extend(string, true, false, {
/***
* @method insert()
* @alias add
*
***/
'insert': string.prototype.add
});
buildBase64('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=');