1 /* 2 * Copyright (C) 2011 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26 var base = base || {}; 27 28 (function(){ 29 30 base.endsWith = function(string, suffix) 31 { 32 if (suffix.length > string.length) 33 return false; 34 var expectedIndex = string.length - suffix.length; 35 return string.lastIndexOf(suffix) == expectedIndex; 36 }; 37 38 base.joinPath = function(parent, child) 39 { 40 if (parent.length == 0) 41 return child; 42 return parent + '/' + child; 43 }; 44 45 base.dirName = function(path) 46 { 47 var directoryIndex = path.lastIndexOf('/'); 48 if (directoryIndex == -1) 49 return path; 50 return path.substr(0, directoryIndex); 51 }; 52 53 base.trimExtension = function(url) 54 { 55 var index = url.lastIndexOf('.'); 56 if (index == -1) 57 return url; 58 return url.substr(0, index); 59 } 60 61 base.uniquifyArray = function(array) 62 { 63 var seen = {}; 64 var result = []; 65 $.each(array, function(index, value) { 66 if (seen[value]) 67 return; 68 seen[value] = true; 69 result.push(value); 70 }); 71 return result; 72 }; 73 74 base.flattenArray = function(arrayOfArrays) 75 { 76 if (!arrayOfArrays.length) 77 return []; 78 return arrayOfArrays.reduce(function(left, right) { 79 return left.concat(right); 80 }); 81 }; 82 83 base.filterDictionary = function(dictionary, predicate) 84 { 85 var result = {}; 86 87 for (var key in dictionary) { 88 if (predicate(key)) 89 result[key] = dictionary[key]; 90 } 91 92 return result; 93 }; 94 95 base.mapDictionary = function(dictionary, functor) 96 { 97 var result = {}; 98 99 for (var key in dictionary) { 100 var value = functor(dictionary[key]); 101 if (typeof value !== 'undefined') 102 result[key] = value; 103 } 104 105 return result; 106 }; 107 108 base.filterTree = function(tree, isLeaf, predicate) 109 { 110 var filteredTree = {}; 111 112 function walkSubtree(subtree, directory) 113 { 114 for (var childName in subtree) { 115 var child = subtree[childName]; 116 var childPath = base.joinPath(directory, childName); 117 if (isLeaf(child)) { 118 if (predicate(child)) 119 filteredTree[childPath] = child; 120 continue; 121 } 122 walkSubtree(child, childPath); 123 } 124 } 125 126 walkSubtree(tree, ''); 127 return filteredTree; 128 }; 129 130 base.forEachDirectory = function(pathList, callback) 131 { 132 var pathsByDirectory = {}; 133 pathList.forEach(function(path) { 134 var directory = base.dirName(path); 135 pathsByDirectory[directory] = pathsByDirectory[directory] || []; 136 pathsByDirectory[directory].push(path); 137 }); 138 Object.keys(pathsByDirectory).sort().forEach(function(directory) { 139 var paths = pathsByDirectory[directory]; 140 callback(directory + ' (' + paths.length + ' tests)', paths); 141 }); 142 }; 143 144 base.parseJSONP = function(jsonp) 145 { 146 if (!jsonp) 147 return {}; 148 149 if (!jsonp.match(/^[^{[]*\(/)) 150 return JSON.parse(jsonp); 151 152 var startIndex = jsonp.indexOf('(') + 1; 153 var endIndex = jsonp.lastIndexOf(')'); 154 if (startIndex == 0 || endIndex == -1) 155 return {}; 156 return JSON.parse(jsonp.substr(startIndex, endIndex - startIndex)); 157 }; 158 159 base.RequestTracker = function(requestsInFlight, callback, args) 160 { 161 this._requestsInFlight = requestsInFlight; 162 this._callback = callback; 163 this._args = args || []; 164 this._tryCallback(); 165 }; 166 167 base.RequestTracker.prototype = { 168 _tryCallback: function() 169 { 170 if (!this._requestsInFlight && this._callback) 171 this._callback.apply(null, this._args); 172 }, 173 requestComplete: function() 174 { 175 --this._requestsInFlight; 176 this._tryCallback(); 177 } 178 } 179 180 base.callInParallel = function(functionList, callback) 181 { 182 var requestTracker = new base.RequestTracker(functionList.length, callback); 183 184 $.each(functionList, function(index, func) { 185 func(function() { 186 requestTracker.requestComplete(); 187 }); 188 }); 189 }; 190 191 base.AsynchronousCache = function(fetch) 192 { 193 this._fetch = fetch; 194 this._dataCache = {}; 195 this._callbackCache = {}; 196 }; 197 198 base.AsynchronousCache.prototype.get = function(key, callback) 199 { 200 var self = this; 201 202 if (self._dataCache[key]) { 203 // FIXME: Consider always calling callback asynchronously. 204 callback(self._dataCache[key]); 205 return; 206 } 207 208 if (key in self._callbackCache) { 209 self._callbackCache[key].push(callback); 210 return; 211 } 212 213 self._callbackCache[key] = [callback]; 214 215 self._fetch.call(null, key, function(data) { 216 self._dataCache[key] = data; 217 218 var callbackList = self._callbackCache[key]; 219 delete self._callbackCache[key]; 220 221 callbackList.forEach(function(cachedCallback) { 222 cachedCallback(data); 223 }); 224 }); 225 }; 226 227 base.AsynchronousCache.prototype.clear = function() 228 { 229 this._dataCache = {}; 230 this._callbackCache = {}; 231 } 232 233 /* 234 Maintains a dictionary of items, tracking their updates and removing items that haven't been updated. 235 An "update" is a call to the "update" method. 236 To remove stale items, call the "remove" method. It will remove all 237 items that have not been been updated since the last call of "remove". 238 */ 239 base.UpdateTracker = function() 240 { 241 this._items = {}; 242 this._updated = {}; 243 } 244 245 base.UpdateTracker.prototype = { 246 /* 247 Update an {key}/{item} pair. You can make the dictionary act as a set and 248 skip the {item}, in which case the {key} is also the {item}. 249 */ 250 update: function(key, object) 251 { 252 object = object || key; 253 this._items[key] = object; 254 this._updated[key] = 1; 255 }, 256 exists: function(key) 257 { 258 return !!this.get(key); 259 }, 260 get: function(key) 261 { 262 return this._items[key]; 263 }, 264 length: function() 265 { 266 return Object.keys(this._items).length; 267 }, 268 /* 269 Callback parameters are: 270 - item 271 - key 272 - updated, which is true if the item was updated after last purge() call. 273 */ 274 forEach: function(callback, thisObject) 275 { 276 if (!callback) 277 return; 278 279 Object.keys(this._items).sort().forEach(function(key) { 280 var item = this._items[key]; 281 callback.call(thisObject || item, item, key, !!this._updated[key]); 282 }, this); 283 }, 284 purge: function(removeCallback, thisObject) { 285 removeCallback = removeCallback || function() {}; 286 this.forEach(function(item, key, updated) { 287 if (updated) 288 return; 289 removeCallback.call(thisObject || item, item); 290 delete this._items[key]; 291 }, this); 292 this._updated = {}; 293 } 294 } 295 296 // Based on http://src.chromium.org/viewvc/chrome/trunk/src/chrome/browser/resources/shared/js/cr/ui.js 297 base.extends = function(base, prototype) 298 { 299 var extended = function() { 300 var element = typeof base == 'string' ? document.createElement(base) : base.call(this); 301 extended.prototype.__proto__ = element.__proto__; 302 element.__proto__ = extended.prototype; 303 var singleton = element.init && element.init.apply(element, arguments); 304 if (singleton) 305 return singleton; 306 return element; 307 } 308 309 extended.prototype = prototype; 310 return extended; 311 } 312 313 function createRelativeTimeDescriptor(divisorInMilliseconds, unit) 314 { 315 return function(delta) { 316 var deltaInUnits = delta / divisorInMilliseconds; 317 return (deltaInUnits).toFixed(0) + ' ' + unit + (deltaInUnits >= 1.5 ? 's' : '') + ' ago'; 318 } 319 } 320 321 var kMinuteInMilliseconds = 60 * 1000; 322 var kRelativeTimeSlots = [ 323 { 324 maxMilliseconds: kMinuteInMilliseconds, 325 describe: function(delta) { return 'Just now'; } 326 }, 327 { 328 maxMilliseconds: 60 * kMinuteInMilliseconds, 329 describe: createRelativeTimeDescriptor(kMinuteInMilliseconds, 'minute') 330 }, 331 { 332 maxMilliseconds: 24 * 60 * kMinuteInMilliseconds, 333 describe: createRelativeTimeDescriptor(60 * kMinuteInMilliseconds, 'hour') 334 }, 335 { 336 maxMilliseconds: Number.MAX_VALUE, 337 describe: createRelativeTimeDescriptor(24 * 60 * kMinuteInMilliseconds, 'day') 338 } 339 ]; 340 341 /* 342 Represent time as descriptive text, relative to now and gradually decreasing in precision: 343 delta < 1 minutes => Just Now 344 delta < 60 minutes => X minute[s] ago 345 delta < 24 hours => X hour[s] ago 346 delta < inf => X day[s] ago 347 */ 348 base.relativizeTime = function(time) 349 { 350 var result; 351 var delta = new Date().getTime() - time; 352 kRelativeTimeSlots.some(function(slot) { 353 if (delta >= slot.maxMilliseconds) 354 return false; 355 356 result = slot.describe(delta); 357 return true; 358 }); 359 return result; 360 } 361 362 base.getURLParameter = function(name) 363 { 364 var match = RegExp(name + '=' + '(.+?)(&|$)').exec(location.search); 365 if (!match) 366 return null; 367 return decodeURI(match[1]) 368 } 369 370 base.underscoredBuilderName = function(builderName) 371 { 372 return builderName.replace(/[ .()]/g, '_'); 373 } 374 375 base.createLinkNode = function(url, textContent, opt_target) 376 { 377 var link = document.createElement('a'); 378 link.href = url; 379 if (opt_target) 380 link.target = opt_target; 381 link.appendChild(document.createTextNode(textContent)); 382 return link; 383 } 384 385 })(); 386