sugar
Version:
A Javascript library for working with native objects.
1,159 lines (1,079 loc) • 37.8 kB
JavaScript
/***
* String module
* @dependency core
*
***/
/***
* @method has[Script]()
* @returns Boolean
* @short Returns true if the string contains any characters in that script.
*
* @set
* hasArabic
* hasCyrillic
* hasGreek
* hasHangul
* hasHan
* hasKanji
* hasHebrew
* hasHiragana
* hasKana
* hasKatakana
* hasLatin
* hasThai
* hasDevanagari
*
* @example
*
* 'أتكلم'.hasArabic() -> true
* 'визит'.hasCyrillic() -> true
* '잘 먹겠습니다!'.hasHangul() -> true
* 'ミックスです'.hasKatakana() -> true
* "l'année".hasLatin() -> true
*
***
* @method is[Script]()
* @returns Boolean
* @short Returns true if the string contains only characters in that script. Whitespace is ignored.
*
* @set
* isArabic
* isCyrillic
* isGreek
* isHangul
* isHan
* isKanji
* isHebrew
* isHiragana
* isKana
* isKatakana
* isKatakana
* isThai
* isDevanagari
*
* @example
*
* 'أتكلم'.isArabic() -> true
* 'визит'.isCyrillic() -> true
* '잘 먹겠습니다!'.isHangul() -> true
* 'ミックスです'.isKatakana() -> false
* "l'année".isLatin() -> true
*
***/
var unicodeScripts = [
{ names: ['Arabic'], source: '\u0600-\u06FF' },
{ names: ['Cyrillic'], source: '\u0400-\u04FF' },
{ names: ['Devanagari'], source: '\u0900-\u097F' },
{ names: ['Greek'], source: '\u0370-\u03FF' },
{ names: ['Hangul'], source: '\uAC00-\uD7AF\u1100-\u11FF' },
{ names: ['Han','Kanji'], source: '\u4E00-\u9FFF\uF900-\uFAFF' },
{ names: ['Hebrew'], source: '\u0590-\u05FF' },
{ names: ['Hiragana'], source: '\u3040-\u309F\u30FB-\u30FC' },
{ names: ['Kana'], source: '\u3040-\u30FF\uFF61-\uFF9F' },
{ names: ['Katakana'], source: '\u30A0-\u30FF\uFF61-\uFF9F' },
{ names: ['Latin'], source: '\u0001-\u007F\u0080-\u00FF\u0100-\u017F\u0180-\u024F' },
{ names: ['Thai'], source: '\u0E00-\u0E7F' }
];
var widthConversionRanges = [
{ type: 'a', shift: 65248, start: 65, end: 90 },
{ type: 'a', shift: 65248, start: 97, end: 122 },
{ type: 'n', shift: 65248, start: 48, end: 57 },
{ type: 'p', shift: 65248, start: 33, end: 47 },
{ type: 'p', shift: 65248, start: 58, end: 64 },
{ type: 'p', shift: 65248, start: 91, end: 96 },
{ type: 'p', shift: 65248, start: 123, end: 126 }
];
var ZenkakuTable = {};
var HankakuTable = {};
var allHankaku = /[\u0020-\u00A5]|[\uFF61-\uFF9F][゙゚]?/g;
var allZenkaku = /[\u3000-\u301C]|[\u301A-\u30FC]|[\uFF01-\uFF60]|[\uFFE0-\uFFE6]/g;
var hankakuPunctuation = '。、「」¥¢£';
var zenkakuPunctuation = '。、「」¥¢£';
var voicedKatakana = /[カキクケコサシスセソタチツテトハヒフヘホ]/;
var semiVoicedKatakana = /[ハヒフヘホヲ]/;
var hankakuKatakana = 'アイウエオァィゥェォカキクケコサシスセソタチツッテトナニヌネノハヒフヘホマミムメモヤャユュヨョラリルレロワヲンー・';
var zenkakuKatakana = 'アイウエオァィゥェォカキクケコサシスセソタチツッテトナニヌネノハヒフヘホマミムメモヤャユュヨョラリルレロワヲンー・';
function buildUnicodeScripts() {
unicodeScripts.forEach(function(s) {
var is = regexp('^['+s.source+'\\s]+$');
var has = regexp('['+s.source+']');
s.names.forEach(function(name) {
defineProperty(string.prototype, 'is' + name, function() { return is.test(this.trim()); });
defineProperty(string.prototype, 'has' + name, function() { return has.test(this); });
});
});
}
function convertCharacterWidth(str, args, reg, table) {
var mode = multiArgs(args).join('');
mode = mode.replace(/all/, '').replace(/(\w)lphabet|umbers?|atakana|paces?|unctuation/g, '$1');
return str.replace(reg, function(c) {
if(table[c] && (!mode || mode.has(table[c].type))) {
return table[c].to;
} else {
return c;
}
});
}
function buildWidthConversionTables() {
var hankaku;
widthConversionRanges.forEach(function(r) {
getRange(r.start, r.end, function(n) {
setWidthConversion(r.type, chr(n), chr(n + r.shift));
});
});
zenkakuKatakana.each(function(c, i) {
hankaku = hankakuKatakana.charAt(i);
setWidthConversion('k', hankaku, c);
if(c.match(voicedKatakana)) {
setWidthConversion('k', hankaku + '゙', c.shift(1));
}
if(c.match(semiVoicedKatakana)) {
setWidthConversion('k', hankaku + '゚', c.shift(2));
}
});
zenkakuPunctuation.each(function(c, i) {
setWidthConversion('p', hankakuPunctuation.charAt(i), c);
});
setWidthConversion('k', 'ヴ', 'ヴ');
setWidthConversion('k', 'ヺ', 'ヺ');
setWidthConversion('s', ' ', ' ');
}
function setWidthConversion(type, half, full) {
ZenkakuTable[half] = { type: type, to: full };
HankakuTable[full] = { type: type, to: half };
}
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;
}
}
function buildString() {
buildBase64('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=');
buildWidthConversionTables();
buildUnicodeScripts();
}
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
*
* '<p>some text</p>'.escapeHTML() -> '<p>some text</p>'
* 'one & two'.escapeHTML() -> 'one & two'
*
***/
'escapeHTML': function() {
return this.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
},
/***
* @method unescapeHTML([partial] = false)
* @returns String
* @short Restores escaped HTML characters.
* @example
*
* '<p>some text</p>'.unescapeHTML() -> '<p>some text</p>'
* 'one & two'.unescapeHTML() -> 'one & two'
*
***/
'unescapeHTML': function() {
return this.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
},
/***
* @method encodeBase64()
* @returns String
* @short Encodes the string into base 64 encoding.
* @extra This methods 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 base 64 encoding.
* @extra This methods 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(<n>)
* @returns Array
* @short Shifts each character in the string <n> 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<this.length; i++) {
var code = this.charCodeAt(i);
codes.push(code);
if(fn) fn.call(this, code, i);
}
return codes;
},
/***
* @method chars([fn])
* @returns Array
* @short Runs callback [fn] against each character in the string. Returns an array of characters.
* @example
*
* 'jumpy'.chars() -> ['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(<find>, [case] = true)
* @returns Boolean
* @short Returns true if the string starts with <find>.
* @extra <find> 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(<find>, [case] = true)
* @returns Boolean
* @short Returns true if the string ends with <find>.
* @extra <find> 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(<find>)
* @returns Boolean
* @short Returns true if the string matches <find>.
* @extra <find> 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(<str>, [index] = length)
* @returns String
* @short Adds <str> 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(<f>)
* @returns String
* @short Removes any part of the string that matches <f>.
* @extra <f> 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 hankaku([mode] = 'all')
* @returns String
* @short Converts full-width characters (zenkaku) to half-width (hankaku).
* @extra [mode] accepts any combination of "a" (alphabet), "n" (numbers), "k" (katakana), "s" (spaces), "p" (punctuation), or "all".
* @example
*
* 'タロウ YAMADAです!'.hankaku() -> 'タロウ YAMADAです!'
* 'タロウ YAMADAです!'.hankaku('a') -> 'タロウ YAMADAです!'
* 'タロウ YAMADAです!'.hankaku('alphabet') -> 'タロウ YAMADAです!'
* 'タロウです! 25歳です!'.hankaku('katakana', 'numbers') -> 'タロウです! 25歳です!'
* 'タロウです! 25歳です!'.hankaku('k', 'n') -> 'タロウです! 25歳です!'
* 'タロウです! 25歳です!'.hankaku('kn') -> 'タロウです! 25歳です!'
* 'タロウです! 25歳です!'.hankaku('sp') -> 'タロウです! 25歳です!'
*
***/
'hankaku': function() {
return convertCharacterWidth(this, arguments, allZenkaku, HankakuTable);
},
/***
* @method zenkaku([mode] = 'all')
* @returns String
* @short Converts half-width characters (hankaku) to full-width (zenkaku).
* @extra [mode] accepts any combination of "a" (alphabet), "n" (numbers), "k" (katakana), "s" (spaces), "p" (punctuation), or "all".
* @example
*
* 'タロウ YAMADAです!'.zenkaku() -> 'タロウ YAMADAです!'
* 'タロウ YAMADAです!'.zenkaku('a') -> 'タロウ YAMADAです!'
* 'タロウ YAMADAです!'.zenkaku('alphabet') -> 'タロウ YAMADAです!'
* 'タロウです! 25歳です!'.zenkaku('katakana', 'numbers') -> 'タロウです! 25歳です!'
* 'タロウです! 25歳です!'.zenkaku('k', 'n') -> 'タロウです! 25歳です!'
* 'タロウです! 25歳です!'.zenkaku('kn') -> 'タロウです! 25歳です!'
* 'タロウです! 25歳です!'.zenkaku('sp') -> 'タロウです! 25歳です!'
*
***/
'zenkaku': function() {
return convertCharacterWidth(this, arguments, allHankaku, ZenkakuTable);
},
/***
* @method hiragana([all] = true)
* @returns String
* @short Converts katakana into hiragana.
* @extra If [all] is false, only full-width katakana will be converted.
* @example
*
* 'カタカナ'.hiragana() -> 'かたかな'
* 'コンニチハ'.hiragana() -> 'こんにちは'
* 'カタカナ'.hiragana() -> 'かたかな'
* 'カタカナ'.hiragana(false) -> 'カタカナ'
*
***/
'hiragana': function(all) {
var str = this;
if(all !== false) {
str = str.zenkaku('k');
}
return str.replace(/[\u30A1-\u30F6]/g, function(c) {
return c.shift(-96);
});
},
/***
* @method katakana()
* @returns String
* @short Converts hiragana into katakana.
* @example
*
* 'かたかな'.katakana() -> 'カタカナ'
* 'こんにちは'.katakana() -> 'コンニチハ'
*
***/
'katakana': function() {
return this.replace(/[\u3041-\u3096]/g, function(c) {
return c.shift(96);
});
},
/***
* @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(<index>, [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'
* 'luckly charms'.at(1,3,5,7) -> ['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.
* @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
*
* '<p>just <b>some</b> text</p>'.stripTags() -> 'just some text'
* '<p>just <b>some</b> text</p>'.stripTags('p') -> 'just <b>some</b> 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
*
* '<p>just <b>some</b> text</p>'.removeTags() -> ''
* '<p>just <b>some</b> text</p>'.removeTags('b') -> '<p>just text</p>'
*
***/
'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(<length>, [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](<padding> = '', [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(<obj1>, <obj2>, ...)
* @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.
* @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;
});
}
});
extend(string, true, function(s) { return isRegExp(s); }, {
/*
* Many thanks to Steve Levithan here for a ton of inspiration and work dealing with
* cross browser Regex splitting. http://blog.stevenlevithan.com/archives/cross-browser-split
*/
/***
* @method split([separator], [limit])
* @returns Array
* @short Splits the string by [separator] into an Array.
* @extra This method is native to Javascript, but Sugar patches it to provide cross-browser reliability when splitting on a regex.
* @example
*
* 'comma,separated,values'.split(',') -> ['comma','separated','values']
* 'a,b|c>d'.split(/[,|>]/) -> ['multi','separated','values']
*
***/
'split': function(separator, limit) {
var output = [];
var lastLastIndex = 0;
var flags = getRegExpFlags(separator, 'g');
// make `global` and avoid `lastIndex` issues by working with a copy
var separator = new regexp(separator.source, flags);
var separator2, match, lastIndex, lastLength;
if(!regexp.NPCGSupport) {
// doesn't need /g or /y, but they don't hurt
separator2 = regexp("^" + separator.source + "$(?!\\s)", flags);
}
if(isUndefined(limit) || limit < 0) {
limit = Infinity;
} else {
limit = floor(limit);
if(!limit) return [];
}
while (match = separator.exec(this)) {
lastIndex = match.index + match[0].length; // `separator.lastIndex` is not reliable cross-browser
if(lastIndex > lastLastIndex) {
output.push(this.slice(lastLastIndex, match.index));
// fix browsers whose `exec` methods don't consistently return `undefined` for nonparticipating capturing groups
if(!regexp.NPCGSupport && match.length > 1) {
match[0].replace(separator2, function () {
for (var i = 1; i < arguments.length - 2; i++) {
if(isUndefined(arguments[i])) {
match[i] = Undefined;
}
}
});
}
if(match.length > 1 && match.index < this.length) {
array.prototype.push.apply(output, match.slice(1));
}
lastLength = match[0].length;
lastLastIndex = lastIndex;
if(output.length >= limit) {
break;
}
}
if(separator.lastIndex === match.index) {
separator.lastIndex++; // avoid an infinite loop
}
}
if(lastLastIndex === this.length) {
if(lastLength || !separator.test('')) output.push('');
} else {
output.push(this.slice(lastLastIndex));
}
return output.length > limit ? output.slice(0, limit) : output;
}
});
// Aliases
extend(string, true, false, {
/***
* @method insert()
* @alias add
*
***/
'insert': string.prototype.add
});
buildString();