Home | History | Annotate | Download | only in empiricism
      1 var notify = [];
      2 var experimentIdCounter = 0;
      3 /**
      4  * The questions above are answered by running a bunch of experiments
      5  * exhaustively for all combinations of HTML element names.
      6  *
      7  * @param makeHtmlString takes one or more element names.
      8  *    Its {@code length} property specifies its arity, and runExperiment
      9  *    calls it iteratively with every permutation of length element names.
     10  * @param checkDom receives the element names passed to makeHtmlString,
     11  *    an HTML document body created by parsing the HTML from makeHtmlString
     12  *    and initialResult/return value from last call to checkDom.
     13  * @param initialResult the first result value to pass to checkDom.
     14  * @param opt_elementNames an array of element names which defaults to
     15  *    window.elementNames.
     16  */
     17 function runExperiment(makeHtmlString, checkDom, initialResult, onResult,
     18                        opt_elementNames) {
     19   var experimentIndex = ++experimentIdCounter;
     20   var iframes = document.getElementById('experiment-iframes');
     21   var iframe = document.createElement('iframe');
     22   iframes.appendChild(iframe);
     23 
     24   var elementNames = opt_elementNames || window.elementNames;
     25 
     26   var nElements = elementNames.length;
     27   var arity = makeHtmlString.length;
     28   var nRuns = Math.pow(nElements, arity);
     29   var runIndex = 0;
     30   var paramIndices = new Array(arity);
     31   var paramValues  = new Array(arity);
     32   for (var i = 0; i < arity; ++i) {
     33     paramIndices[i] = 0;
     34     paramValues[i] = elementNames[0];
     35   }
     36   var exhausted = nRuns === 0;
     37 
     38   var progressCounterContainer =
     39     document.getElementById('experiment-progress-counter');
     40 
     41   var startTime = Date.now();
     42   var lastProgressUpdateTime = startTime;
     43 
     44   var result = initialResult;
     45 
     46   var progressCounter;
     47   if (progressCounterContainer) {
     48     progressCounter = document.createElement('li');
     49     progressCounter.style.width = '0';
     50     progressCounterContainer.appendChild(progressCounter);
     51   }
     52 
     53   function advance() {
     54     // Advance to next permutation.
     55     var i;
     56     for (i = arity; --i >= 0;) {
     57       if (++paramIndices[i] < nElements) {
     58         paramValues[i] = elementNames[paramIndices[i]];
     59         break;
     60       }
     61       paramIndices[i] = 0;
     62       paramValues [i] = elementNames[0];
     63     }
     64     ++runIndex;
     65     if (progressCounter) {
     66       var now = Date.now();
     67       if (now - lastProgressUpdateTime > 250 ) {
     68         var ratio = runIndex / nRuns;
     69         progressCounter.style.width = (100 * ratio).toFixed(2) + '%';
     70         lastProgressUpdateTime = now;
     71         var timeSoFar = now - startTime;
     72         if (timeSoFar > 5000) {
     73           // Assuming time per run is constant:
     74           // total_time / nRuns = time_so_far / runIndex
     75           // total_time = time_so_far * nRuns / runIndex
     76           //            = time_so_far / ratio
     77           // eta = total_time - time_so_far
     78           //     = time_so_far / ratio - time_so_far
     79           //     = time_so_far * (1/ratio - 1)
     80           var eta = timeSoFar * (1 / ratio - 1);
     81           progressCounter.innerHTML = eta > 250
     82               ? 'ETA:' + (eta / 1000).toFixed(1) + 's' : '';
     83         }
     84       }
     85     }
     86     exhausted = i < 0;
     87   }
     88 
     89   function step() {
     90     var htmlString = null;
     91     // Try to generate an HTML string.
     92     // The maker can return a nullish value to abort or punt on an experiment,
     93     // so we loop until we find work to do.
     94     while (!exhausted) {
     95       paramValues.length = arity;
     96       htmlString = makeHtmlString.apply(null, paramValues);
     97       if (htmlString != null) {
     98         break;
     99       }
    100       advance();
    101     }
    102 
    103     if (htmlString == null) { 
    104       var endTime = Date.now();
    105       console.log('experiment took %d millis for %d runs',
    106                   (endTime - startTime), nRuns);
    107       if (progressCounter) {
    108         setTimeout(function () {
    109           iframes.removeChild(iframe);
    110           progressCounterContainer.removeChild(progressCounter);
    111         }, 250);
    112       }
    113       onResult(result);
    114     } else {
    115       var notifyIndex = notify.indexOf(void 0);
    116       if (notifyIndex < 0) { notifyIndex = notify.length; }
    117       notify[notifyIndex] = function () {
    118         notify[notifyIndex] = void 0;
    119 
    120         // Process result
    121         paramValues[arity] = iframe.contentDocument.body;
    122         paramValues[arity + 1] = result;
    123         result = checkDom.apply(null, paramValues);
    124         paramValues.length = arity;
    125 
    126         // Requeue the next step on the parent frames event queue.
    127         setTimeout(function () { advance(); step(); }, 0);
    128       };
    129       // Start the iframe parsing its body.
    130       iframe.srcdoc = (
    131         '<!doctype html><html><head></head>'
    132         + '<body onload="parent.notify[' + notifyIndex + ']()">'
    133         + htmlString
    134       );
    135     }
    136   }
    137   step();
    138 }
    139 
    140 function formatDataToJsonHTML(data) {
    141   var out = [];
    142   var htmlForNullValue = '<span class="json-kw">null</span>';
    143   var htmlForErrorValue = '<span class="json-kw json-err">null</span>';
    144   var depth = 0;
    145   var spaces = '                ';
    146   format(data);
    147   return out.join('');
    148 
    149   function format(v) {
    150     if (v == null) {
    151       out.push(htmlForNullValue);
    152       return;
    153     }
    154     var t = typeof v;
    155     if (t === 'boolean') {
    156       out.push('<span class="json-kw">', v, '</span>');
    157     } else if (t === 'number') {
    158       if (isFinite(v)) {
    159         out.push('<span class="json-val">', v, '</span>');
    160       } else {
    161         out.push(htmlForErrorValue);
    162       }
    163     } else if (t === 'string' || v instanceof String) {
    164       var token = JSON.stringify(String(v));
    165       token = token.replace(/&/g, '&amp;').replace(/</g, '&lt;');
    166       out.push('<span class="json-str">', token, '</span>');
    167     } else {
    168       var length = v.length;
    169       var isSeries = ('number' === typeof length
    170                       && length === (length & 0x7fffffff));
    171       // Don't put properties on their own line if there are only a few.
    172       var inlinePropLimit = isSeries ? 8 : 4;
    173       var inline = true;
    174       var numProps = 0;
    175       for (var k in v) {
    176         if (!Object.hasOwnProperty.call(v, k)) { continue; }
    177         var propValue = v[k];
    178         if ((propValue != null && typeof propValue == 'object')
    179             || ++numProps > inlinePropLimit) {
    180           inline = false;
    181           break;
    182         }
    183       }
    184       // Put the appropriate white-space inside brackets and after commas.
    185       function maybeIndent(afterComma) {
    186         if (inline) {
    187           if (afterComma) { out.push(' '); }
    188         } else {
    189           out.push('\n');
    190           var nSpaces = depth * 2;
    191           while (nSpaces > 0) {
    192             var nToPush = Math.min(nSpaces, spaces.length);
    193             out.push(spaces.substring(0, nToPush));
    194             nSpaces -= nToPush;
    195           }
    196         }
    197       }
    198       var onclick = depth
    199         ? ' onclick="return toggleJsonBlock(this, event)"'
    200         : '';
    201       // Mark blocks so that we can do expandos on collections.
    202       out.push('<span class="json-ext json-block-', depth,
    203                depth === 0 || inline ? ' json-nocollapse' : '',
    204                '"', onclick, '>',
    205                isSeries ? '[' : '{',
    206                // Emit link-like ellipses that can serve as a button for
    207                // expando-ness.
    208                '<span class="json-ell">&hellip;</span>',
    209                '<span class="json-int">');
    210       ++depth;
    211       if (isSeries) {
    212         for (var i = 0; i < length; ++i) {
    213           if (i) { out.push(','); }
    214           maybeIndent(i !== 0);
    215           format(v[i]);
    216         }
    217       } else {
    218         var needsComma = false;
    219         for (var k in v) {
    220           if (!Object.hasOwnProperty.call(v, k)) { continue; }
    221           if (needsComma) {
    222             out.push(',');
    223           }
    224           maybeIndent(needsComma);
    225           out.push('<span class="json-prop">');
    226           format(String(k));
    227           out.push(': ');
    228           format(v[k]);
    229           out.push('</span>');
    230           needsComma = true;
    231         }
    232       }
    233       --depth;
    234       maybeIndent(false);
    235       out.push('</span>', isSeries ? ']' : '}', '</span>');
    236     }
    237   }
    238 }
    239 
    240 function displayJson(data, container) {
    241   container.innerHTML = formatDataToJsonHTML(data);
    242 }
    243 
    244 function toggleJsonBlock(el, event) {
    245   event && event.stopPropagation && event.stopPropagation();
    246   var className = el.className;
    247   var classNameCollapsed = className.replace(/\bjson-expanded\b/g, '');
    248   className = className === classNameCollapsed
    249       ? className + ' json-expanded' : classNameCollapsed;
    250   className = className.replace(/^ +| +$| +( [^ ])/g, "$1");
    251   el.className = className;
    252   return false;
    253 }
    254 
    255 function Promise() {
    256   if (!(this instanceof Promise)) { return new Promise(); }
    257   this.paused = [];
    258   this.satisfy = function () {
    259     var paused = this.paused;
    260 console.log('satisfying ' + paused.length);
    261     for (var i = 0, n = paused.length; i < n; ++i) {
    262       setTimeout(paused[i], 0);
    263     }
    264     this.paused.length = 0;
    265   };
    266 }
    267 Promise.prototype.toString = function () { return "Promise"; };
    268 function when(f, var_args) {
    269   var unsatisfied = [];
    270   for (var i = 1, n = arguments.length; i < n; ++i) {
    271     var argument = arguments[i];
    272     if (argument instanceof Promise) {
    273       unsatisfied.push(argument);
    274     }
    275   }
    276   var nToWaitFor = unsatisfied.length;
    277   if (nToWaitFor) {
    278     var pauser = function pauser() {
    279       if (!--nToWaitFor) {
    280         setTimeout(f, 0);
    281       }
    282     };
    283     for (var j = 0; j < nToWaitFor; ++j) {
    284       unsatisfied[j].paused.push(pauser);
    285     }
    286     unsatisfied = null;
    287   } else {
    288     setTimeout(f, 0);
    289   }
    290 }
    291 
    292 function newBlankObject() {
    293   return (Object.create || Object)(null);
    294 }
    295 
    296 function getOwn(o, k, opt_default) {
    297   return Object.hasOwnProperty.call(o, k) ? o[k] : opt_default;
    298 }
    299 
    300 function breadthFirstSearch(start, isEnd, eq, adjacent) {
    301   var stack = [{ node: start, next: null }];
    302   while (stack.length) {
    303     var candidate = stack.shift();
    304     if (isEnd(candidate.node)) {
    305       var path = [candidate.node];
    306       while (candidate.next) {
    307         candidate = candidate.next;
    308         path.push(candidate.node);
    309       }
    310       return path;
    311     }
    312     var adjacentNodes = adjacent(candidate.node);
    313     adj:
    314     for (var i = 0, n = adjacentNodes.length; i < n; ++i) {
    315       var adjacentNode = adjacentNodes[i];
    316       for (var dupe = candidate; dupe; dupe = dupe.next) {
    317         if (eq(dupe.node, adjacentNode)) { continue adj; }
    318       }
    319       stack.push({ node: adjacentNode, next: candidate });
    320     }
    321   }
    322   return null;
    323 }
    324 
    325 function reverseMultiMap(multimap) {
    326   var reverse = newBlankObject();
    327   for (var k in multimap) {
    328     if (Object.hasOwnProperty.call(multimap, k)) {
    329       var values = multimap[k];
    330       for (var i = 0, n = values.length; i < n; ++i) {
    331         var value = values[i];
    332         var reverseKeys = getOwn(reverse, value) || [];
    333         reverse[value] = reverseKeys;
    334         reverseKeys.push(k);
    335       }
    336     }
    337   }
    338   return reverse;
    339 }
    340 
    341 function innerTextOf(element) {
    342   function appendTextOf(node, out) {
    343     switch (node.nodeType) {
    344       case 1:  // Element
    345         for (var c = node.firstChild; c; c = c.nextSibling) {
    346           appendTextOf(c, out);
    347         }
    348         break;
    349       case 3: case 4: case 6:  // Text / CDATA / Entity
    350         out.push(node.nodeValue);
    351         break;
    352     }
    353   }
    354   var buf = [];
    355   if (element) { appendTextOf(element, buf); }
    356   return buf.join('');
    357 }
    358 
    359 function sortedMultiMap(mm) {
    360   var props = [];
    361   for (var k in mm) {
    362     if (!Object.hasOwnProperty.call(mm, k)) { continue; }
    363     var v = mm[k];
    364     if (v instanceof Array) {
    365       v = v.slice();
    366       v.sort();
    367     }
    368     props.push([k, v]);
    369   }
    370   props.sort(
    371       function (a, b) {
    372         a = a[0];
    373         b = b[0];
    374         if (a < b) { return -1; }
    375         if (b < a) { return 1; }
    376         return 0;
    377       });
    378   var sorted = newBlankObject();
    379   for (var i = 0, n = props.length; i < n; ++i) {
    380     var prop = props[i];
    381     sorted[prop[0]] = prop[1];
    382   }
    383   return sorted;
    384 }
    385 
    386 function makeSet(strs) {
    387   var s = newBlankObject();
    388   for (var i = 0, n = strs.length; i < n; ++i) {
    389     s[strs[i]] = s;
    390   }
    391   return s;
    392 }
    393 
    394 function inSet(s, str) {
    395   return s[str] === s;
    396 }
    397 
    398 function elementContainsComment(el) {
    399   return elementContainsNodeOfType(el, 8);
    400 }
    401 
    402 function elementContainsText(el) {
    403   return elementContainsNodeOfType(el, 3);
    404 }
    405 
    406 function elementContainsNodeOfType(el, nodeType) {
    407   if (el) {
    408     for (var c = el.firstChild; c; c = c.nextSibling) {
    409       if (c.nodeType === nodeType) { return true; }
    410     }
    411     return false;
    412   }
    413 }
    414