Home | History | Annotate | Download | only in scripts
      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