'use strict'; /*@ Ox.char Alias for String.fromCharCode @*/ Ox.char = String.fromCharCode; /*@ Ox.clean Remove leading, trailing and double whitespace from a string > Ox.clean('foo bar') 'foo bar' > Ox.clean(' foo bar ') 'foo bar' > Ox.clean(' foo \n bar ') 'foo\nbar' > Ox.clean(' \nfoo\n\nbar\n ') 'foo\nbar' > Ox.clean(' foo\tbar ') 'foo bar' @*/ Ox.clean = function(string) { return Ox.filter(Ox.map(string.split('\n'), function(string) { return string.replace(/\s+/g, ' ').trim() || ''; })).join('\n'); }; /*@ Ox.codePointAt Returns the code point at a given index (string, index) -> Code point > Ox.codePointAt('\uD83D\uDCA9', 0) 0x1F4A9 @*/ Ox.codePointAt = function(string, index) { var first, length = string.length, ret, second; if (index >= 0 && index < length) { first = string.charCodeAt(index); if (first < 0xD800 || first > 0xDBFF || index == length - 1) { ret = first; } else { second = string.charCodeAt(index + 1); ret = second < 0xDC00 || second > 0xDFFF ? first : ((first - 0xD800) * 0x400) + (second - 0xDC00) + 0x10000; } } return ret; }; /*@ Ox.endsWith Tests if a string ends with a given substring Equivalent to (but faster than) `new RegExp(Ox.escapeRegExp(substring) + '$').test(string)`. (string, substring) -> True if string ends with substring > Ox.endsWith('foobar', 'bar') true > Ox.endsWith('foobar', 'foo') false @*/ Ox.endsWith = function(string, substring) { string = string.toString(); substring = substring.toString(); return string.slice(string.length - substring.length) == substring; }; /*@ Ox.fromCodePoint Returns a string for one or more given code points (codePoint[, codePoint[, ...]]) -> String > Ox.fromCodePoint(102, 111, 111) 'foo' > Ox.fromCodePoint(0x1F4A9) '\uD83D\uDCA9' @*/ Ox.fromCodePoint = function() { var ret = ''; Ox.forEach(arguments, function(number) { if (number < 0 || number > 0x10FFFF || !Ox.isInt(number)) { throw new RangeError(); } if (number < 0x10000) { ret += String.fromCharCode(number); } else { number -= 0x10000; ret += String.fromCharCode((number >> 10) + 0xD800) + String.fromCharCode((number % 0x400) + 0xDC00); } }); return ret; }; /*@ Ox.isValidEmail Tests if a string is a valid e-mail address (str) -> True if the string is a valid e-mail address str Any string > Ox.isValidEmail('foo@bar.com') true > Ox.isValidEmail('foo.bar@foobar.co.uk') true > Ox.isValidEmail('foo@bar') false > Ox.isValidEmail('foo@bar..com') false @*/ Ox.isValidEmail = function(string) { return !!/^[0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6}$/i.test(string); }; /*@ Ox.pad Pad a string to a given length (string[, position], length[, padding]) -> Padded string string String position Position ('left' or 'right') When passing a number as `string`, the default position is 'left'. length Length padding Padding When passing a number as `string`, and leaving out or passing 'left' as `position`, the default padding is '0'. > Ox.pad('foo', 6) 'foo ' > Ox.pad('foo', 'left', 6) ' foo' > Ox.pad('foo', 6, '.') 'foo...' > Ox.pad('foo', 'left', 6, '.') '...foo' > Ox.pad(1, 2) '01' > Ox.pad(1, 2, ' ') ' 1' > Ox.pad(1, 'right', 2) '1 ' > Ox.pad(1, 'right', 2, '_') '1_' > Ox.pad('foo', 6, '123456') 'foo123' > Ox.pad('foo', 'left', 6, '123456') '456foo' > Ox.pad('foobar', 3) 'foo' > Ox.pad('foobar', 'left', 3) 'bar' > Ox.pad('foo', -1) '' @*/ Ox.pad = function(string, position, length, padding) { var hasPosition = Ox.isString(arguments[1]), isNumber = Ox.isNumber(arguments[0]), last = Ox.last(arguments); position = hasPosition ? arguments[1] : isNumber ? 'left' : 'right'; length = Math.max(hasPosition ? arguments[2] : arguments[1], 0); padding = Ox.isString(last) ? last : isNumber && position == 'left' ? '0' : ' '; string = string.toString(); padding = Ox.repeat(padding, length - string.length); return position == 'left' ? (padding + string).slice(-length) : (string + padding).slice(0, length); }; /*@ Ox.parseDuration Takes a formatted duration, returns seconds > Ox.parseDuration('1:02:03:04.05') 93784.05 > Ox.parseDuration('3') 3 > Ox.parseDuration('2:') 120 > Ox.parseDuration('1::') 3600 @*/ Ox.parseDuration = function(string) { return string.split(':').reverse().slice(0, 4).reduce(function(p, c, i) { return p + (parseFloat(c) || 0) * (i == 3 ? 86400 : Math.pow(60, i)); }, 0); }; /*@ Ox.parsePath Returns the components of a path (str) -> Path extension File extension filename Filename pathname Pathname > Ox.parsePath('/foo/bar/foo.bar') {extension: 'bar', filename: 'foo.bar', pathname: '/foo/bar/'} > Ox.parsePath('foo/') {extension: '', filename: '', pathname: 'foo/'} > Ox.parsePath('foo') {extension: '', filename: 'foo', pathname: ''} > Ox.parsePath('.foo') {extension: '', filename: '.foo', pathname: ''} @*/ Ox.parsePath = function(string) { var matches = /^(.+\/)?(.+?(\..+)?)?$/.exec(string); return { pathname: matches[1] || '', filename: matches[2] || '', extension: matches[3] ? matches[3].slice(1) : '' }; }; /*@ Ox.parseSRT Parses an srt subtitle file (str) -> Parsed subtitles in In point (sec) out Out point (sec) text Text str Contents of an srt subtitle file > Ox.parseSRT('1\n01:02:00,000 --> 01:02:03,400\nHello World') [{'in': 3720, out: 3723.4, text: 'Hello World'}] @*/ Ox.parseSRT = function(string, fps) { return string.replace(/\r\n/g, '\n').replace(/\n+$/, '').split('\n\n') .map(function(block) { var lines = block.split('\n'), points; lines.shift(); points = lines.shift().split(' --> ').map(function(point) { return point.replace(',', ':').split(':') .reduce(function(previous, current, index) { return previous + parseInt(current, 10) * [3600, 60, 1, 0.001][index]; }, 0); }); if (fps) { points = points.map(function(point) { return Math.round(point * fps) / fps; }); } return { 'in': points[0], out: points[1], text: lines.join('\n') }; }); }; /*@ Ox.parseURL Takes a URL, returns its components (url) -> URL components hash Hash host Host hostname Hostname origin Origin pathname Pathname port Port protocol Protocol search Search url URL > Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c').hash '#c' > Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c').host 'www.foo.com:8080' > Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c').hostname 'www.foo.com' > Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c').origin 'http://www.foo.com:8080' > Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c').pathname '/bar/index.html' > Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c').port '8080' > Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c').protocol 'http:' > Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c').search '?a=0&b=1' @*/ Ox.parseURL = (function() { var a = document.createElement('a'), keys = ['hash', 'host', 'hostname', 'origin', 'pathname', 'port', 'protocol', 'search']; return function(string) { var ret = {}; a.href = string; keys.forEach(function(key) { ret[key] = a[key]; }); return ret; }; }()); // FIXME: can we get rid of this? Ox.parseUserAgent = function(userAgent) { var aliases = { browser: { 'Firefox': /(Fennec|Firebird|Iceweasel|Minefield|Namoroka|Phoenix|SeaMonkey|Shiretoko)/ }, system: { 'BSD': /(FreeBSD|NetBSD|OpenBSD)/, 'Linux': /(CrOS|MeeGo|webOS)/, 'Unix': /(AIX|HP-UX|IRIX|SunOS)/ } }, names = { browser: { 'chromeframe': 'Chrome Frame', 'MSIE': 'Internet Explorer' }, system: { 'CPU OS': 'iOS', 'iPhone OS': 'iOS', 'Macintosh': 'Mac OS X' } }, regexps = { browser: [ /(Camino)\/(\d+)/, /(chromeframe)\/(\d+)/, /(Chrome)\/(\d+)/, /(Edge)\/(\d+)/, /(Epiphany)\/(\d+)/, /(Firefox)\/(\d+)/, /(Galeon)\/(\d+)/, /(Googlebot)\/(\d+)/, /(Konqueror)\/(\d+)/, /(MSIE) (\d+)/, /(Netscape)\d?\/(\d+)/, /(NokiaBrowser)\/(\d+)/, /(Opera) (\d+)/, /(Opera)\/.+Version\/(\d+)/, /Version\/(\d+).+(Safari)/ ], system: [ /(Android) (\d+)/, /(BeOS)/, /(BlackBerry) (\d+)/, /(Darwin)/, /(BSD) (FreeBSD|NetBSD|OpenBSD)/, /(CPU OS) (\d+)/, /(iPhone OS) (\d+)/, /(Linux).+(CentOS|CrOS|Debian|Fedora|Gentoo|Mandriva|MeeGo|Mint|Red Hat|SUSE|Ubuntu|webOS)/, /(CentOS|CrOS|Debian|Fedora|Gentoo|Mandriva|MeeGo|Mint|Red Hat|SUSE|Ubuntu|webOS).+(Linux)/, /(Linux)/, /(Mac OS X) (10.\d+)/, /(Mac OS X)/, /(Macintosh)/, /(SymbianOS)\/(\d+)/, /(SymbOS)/, /(OS\/2)/, /(Unix) (AIX|HP-UX|IRIX|SunOS)/, /(Unix)/, /(Windows) (NT \d\.\d)/, /(Windows) (95|98|2000|2003|ME|NT|XP)/, // Opera /(Windows).+(Win 9x 4\.90)/, // Firefox /(Windows).+(Win9\d)/, // Firefox /(Windows).+(WinNT4.0)/ // Firefox ] }, versions = { browser: {}, system: { '10.0': '10.0 (Cheetah)', '10.1': '10.1 (Puma)', '10.2': '10.2 (Jaguar)', '10.3': '10.3 (Panther)', '10.4': '10.4 (Tiger)', '10.5': '10.5 (Leopard)', '10.6': '10.6 (Snow Leopard)', '10.7': '10.7 (Lion)', '10.8': '10.8 (Mountain Lion)', '10.9': '10.9 (Mavericks)', '10.10': '10.10 (Yosemite)', '10.11': '10.11 (El Capitan)', 'CrOS': 'Chrome OS', 'NT 4.0': 'NT 4.0 (Windows NT)', 'NT 4.1': 'NT 4.1 (Windows 98)', 'Win 9x 4.90': 'NT 4.9 (Windows ME)', 'NT 5.0': 'NT 5.0 (Windows 2000)', 'NT 5.1': 'NT 5.1 (Windows XP)', 'NT 5.2': 'NT 5.2 (Windows 2003)', 'NT 6.0': 'NT 6.0 (Windows Vista)', 'NT 6.1': 'NT 6.1 (Windows 7)', 'NT 6.2': 'NT 6.2 (Windows 8)', 'NT 6.3': 'NT 6.3 (Windows 8.1)', 'NT 6.4': 'NT 6.4 (Windows 10)', '95': 'NT 4.0 (Windows 95)', 'NT': 'NT 4.0 (Windows NT)', '98': 'NT 4.1 (Windows 98)', 'ME': 'NT 4.9 (Windows ME)', '2000': 'NT 5.0 (Windows 2000)', '2003': 'NT 5.2 (Windows 2003)', 'XP': 'NT 5.1 (Windows XP)', 'Win95': 'NT 4.0 (Windows 95)', 'WinNT4.0': 'NT 4.0 (Windows NT)', 'Win98': 'NT 4.1 (Windows 98)' } }, userAgentData = {}; Ox.forEach(regexps, function(regexps, key) { userAgentData[key] = {name: '', string: '', version: ''}; Ox.forEach(aliases[key], function(regexp, alias) { userAgent = userAgent.replace( regexp, key == 'browser' ? alias : alias + ' $1' ); }); Ox.forEach(regexps, function(regexp) { var matches = userAgent.match(regexp), name, string, swap, version; if (matches) { matches[2] = matches[2] || ''; swap = matches[1].match(/^\d/) || matches[2] == 'Linux'; name = matches[swap ? 2 : 1]; version = matches[swap ? 1 : 2].replace('_', '.'); name = names[key][name] || name, version = versions[key][version] || version; string = name; if (version) { string += ' ' + ( ['BSD', 'Linux', 'Unix'].indexOf(name) > -1 ? '(' + version + ')' : version ); } userAgentData[key] = { name: names[name] || name, string: string, version: versions[version] || version }; return false; // break } }); }); return userAgentData; }; /*@ Ox.repeat Repeat a value multiple times Works for arrays, numbers and strings > Ox.repeat(1, 3) '111' > Ox.repeat('foo', 3) 'foofoofoo' > Ox.repeat([1, 2], 3) [1, 2, 1, 2, 1, 2] > Ox.repeat([{k: 'v'}], 3) [{k: 'v'}, {k: 'v'}, {k: 'v'}] @*/ // FIXME: see https://github.com/paulmillr/es6-shim/blob/master/es6-shim.js // for a faster version Ox.repeat = function(value, times) { var ret; if (Ox.isArray(value)) { ret = []; Ox.loop(times, function() { ret = ret.concat(value); }); } else { ret = times >= 1 ? new Array(times + 1).join(value.toString()) : ''; } return ret; }; /*@ Ox.splice `[].splice` for strings, returns a new string > Ox.splice('12xxxxx89', 2, 5, 3, 4, 5, 6, 7) '123456789' @*/ Ox.splice = function(string, index, remove) { var array = string.split(''); Array.prototype.splice.apply(array, Ox.slice(arguments, 1)); return array.join(''); }; /*@ Ox.startsWith Tests if a string ends with a given substring Equivalent to (but faster than) `new RegExp('^' + Ox.escapeRegExp(substring)).test(string)`. (string, substring) -> True if string starts with substring > Ox.startsWith('foobar', 'foo') true > Ox.startsWith('foobar', 'bar') false @*/ Ox.startsWith = function(string, substring) { string = string.toString(); substring = substring.toString(); return string.slice(0, substring.length) == substring; }; /*@ Ox.toCamelCase Takes a string with '-', '/' or '_', returns a camelCase string > Ox.toCamelCase('foo-bar-baz') 'fooBarBaz' > Ox.toCamelCase('foo/bar/baz') 'fooBarBaz' > Ox.toCamelCase('foo_bar_baz') 'fooBarBaz' @*/ Ox.toCamelCase = function(string) { return string.replace(/[\-\/_][a-z]/g, function(string) { return string[1].toUpperCase(); }); }; /*@ Ox.toDashes Takes a camelCase string, returns a string with dashes > Ox.toDashes('fooBarBaz') 'foo-bar-baz' @*/ Ox.toDashes = function(string) { return string.replace(/[A-Z]/g, function(string) { return '-' + string.toLowerCase(); }); }; /*@ Ox.toSlashes Takes a camelCase string, returns a string with slashes > Ox.toSlashes('fooBarBaz') 'foo/bar/baz' @*/ Ox.toSlashes = function(string) { return string.replace(/[A-Z]/g, function(string) { return '/' + string.toLowerCase(); }); }; /*@ Ox.toTitleCase Returns a string with capitalized words > Ox.toTitleCase('foo') 'Foo' > Ox.toTitleCase('Apple releases iPhone, IBM stock plummets') 'Apple Releases iPhone, IBM Stock Plummets' @*/ Ox.toTitleCase = function(string) { return string.split(' ').map(function(value) { var substring = value.slice(1), lowercase = substring.toLowerCase(); if (substring == lowercase) { value = value.slice(0, 1).toUpperCase() + lowercase; } return value; }).join(' '); }; /*@ Ox.toUnderscores Takes a camelCase string, returns string with underscores > Ox.toUnderscores('fooBarBaz') 'foo_bar_baz' @*/ Ox.toUnderscores = function(string) { return string.replace(/[A-Z]/g, function(string) { return '_' + string.toLowerCase(); }); }; /*@ Ox.truncate Truncate a string to a given length (string[, position], length[, padding]) -> Truncated string string String position Position ('left', 'center' or 'right') length Length padding Padding > Ox.truncate('anticonstitutionellement', 16) 'anticonstitutio…' > Ox.truncate('anticonstitutionellement', 'left', 16) '…itutionellement' > Ox.truncate('anticonstitutionellement', 16, '...') 'anticonstitut...' > Ox.truncate('anticonstitutionellement', 'center', 16, '...') 'anticon...lement' @*/ Ox.truncate = function(string, position, length, padding) { var hasPosition = Ox.isString(arguments[1]), last = Ox.last(arguments); position = hasPosition ? arguments[1] : 'right'; length = hasPosition ? arguments[2] : arguments[1]; padding = Ox.isString(last) ? last : '…'; if (string.length > length) { if (position == 'left') { string = padding + string.slice(padding.length + string.length - length); } else if (position == 'center') { string = string.slice(0, Math.ceil((length - padding.length) / 2)) + padding + string.slice(-Math.floor((length - padding.length) / 2)); } else if (position == 'right') { string = string.slice(0, length - padding.length) + padding; } } return string; }; /*@ Ox.words Splits a string into words, removing punctuation (string) -> <[s]> Array of words string Any string > Ox.words('Let\'s "split" array-likes into key/value pairs--okay?') ['let\'s', 'split', 'array-likes', 'into', 'key', 'value', 'pairs', 'okay'] @*/ Ox.words = function(string) { var array = string.toLowerCase().split(/\b/), length = array.length, startsWithWord = /\w/.test(array[0]); array.forEach(function(v, i) { // find single occurrences of "-" or "'" that are not at the beginning // or end of the string, and join the surrounding words with them if ( i > 0 && i < length - 1 && (v == '-' || v == '\'') ) { array[i + 1] = array[i - 1] + array[i] + array[i + 1]; array[i - 1] = array[i] = ''; } }); // remove elements that have been emptied above array = array.filter(function(v) { return v.length; }); // return words, not spaces or punctuation return array.filter(function(v, i) { return i % 2 == !startsWithWord; }); }; /*@ Ox.wordwrap Wrap a string at word boundaries (string, length) -> Wrapped string (string, length, newline) -> Wrapped string (string, length, balanced) -> Wrapped string (string, length, balanced, newline) -> Wrapped string (string, length, newline, balanced) -> Wrapped string string String length Line length balanced If true, lines will have similar length newline Newline character or string > Ox.wordwrap('Anticonstitutionellement, Paris s\'eveille', 25) 'Anticonstitutionellement, \nParis s\'eveille' > Ox.wordwrap('Anticonstitutionellement, Paris s\'eveille', 25, '
') 'Anticonstitutionellement,
Paris s\'eveille' > Ox.wordwrap('Anticonstitutionellement, Paris s\'eveille', 16, '
') 'Anticonstitution
ellement, Paris
s\'eveille' > Ox.wordwrap('These are short words', 16) 'These are short \nwords' > Ox.wordwrap('These are short words', 16, true) 'These are \nshort words' @*/ Ox.wordwrap = function(string, length) { var balanced, lines, max, newline, words; string = String(string); length = length || 80; balanced = Ox.isBoolean(arguments[2]) ? arguments[2] : Ox.isBoolean(arguments[3]) ? arguments[3] : false; newline = Ox.isString(arguments[2]) ? arguments[2] : Ox.isString(arguments[3]) ? arguments[3] : '\n'; words = string.split(' '); if (balanced) { // balanced lines: test if same number of lines // can be achieved with a shorter line length lines = Ox.wordwrap(string, length, newline).split(newline); if (lines.length > 1) { // test shorter line, unless // that means cutting a word max = Ox.max(words.map(function(word) { return word.length; })); while (length > max) { if (Ox.wordwrap( string, --length, newline ).split(newline).length > lines.length) { length++; break; } } } } lines = ['']; words.forEach(function(word) { var index; if ((lines[lines.length - 1] + word).length <= length) { // word fits in current line lines[lines.length - 1] += word + ' '; } else { if (word.length <= length) { // word fits in next line lines.push(word + ' '); } else { // word is longer than line index = length - lines[lines.length - 1].length; lines[lines.length - 1] += word.slice(0, index); while (index < word.length) { lines.push(word.substr(index, length)); index += length; } lines[lines.length - 1] += ' '; } } }); return lines.join(newline).trim(); };