/*** * @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, function(reg) { return isRegExp(reg) || arguments.length > 2; }, { /*** * @method startsWith(, [pos] = 0, [case] = true) * @returns Boolean * @short Returns true if the string starts with . * @extra may be either a string or regex. Search begins at [pos], which defaults to the entire string. Case sensitive if [case] is true. * @example * * 'hello'.startsWith('hell') -> true * 'hello'.startsWith(/[a-h]/) -> true * 'hello'.startsWith('HELL') -> false * 'hello'.startsWith('ell', 1) -> true * 'hello'.startsWith('HELL', 0, false) -> true * ***/ 'startsWith': function(reg, pos, c) { var str = this, source; if(pos) str = str.slice(pos); if(isUndefined(c)) c = true; source = isRegExp(reg) ? reg.source.replace('^', '') : escapeRegExp(reg); return regexp('^' + source, c ? '' : 'i').test(str); }, /*** * @method endsWith(, [pos] = length, [case] = true) * @returns Boolean * @short Returns true if the string ends with . * @extra may be either a string or regex. Search ends at [pos], which defaults to the entire string. Case sensitive if [case] is true. * @example * * 'jumpy'.endsWith('py') -> true * 'jumpy'.endsWith(/[q-z]/) -> true * 'jumpy'.endsWith('MPY') -> false * 'jumpy'.endsWith('mp', 4) -> false * 'jumpy'.endsWith('MPY', 5, false) -> true * ***/ 'endsWith': function(reg, pos, c) { var str = this, source; if(isDefined(pos)) str = str.slice(0, pos); if(isUndefined(c)) c = true; source = isRegExp(reg) ? reg.source.replace('$', '') : escapeRegExp(reg); return regexp(source + '$', c ? '' : 'i').test(str); } }); 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, '>' ) .replace(/"/g, '"') .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, '>') .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 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 : ['']; flattenedArgs(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+']; flattenedArgs(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' * 'a'.repeat(0) -> '' * ***/ 'repeat': function(num) { var result = '', str = this; if(!isNumber(num) || num < 1) return ''; while (num) { if (num & 1) { result += str; } if (num >>= 1) { str += str; } } return result; }, /*** * @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+/=');