1var notify = [];
2var 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 */
17function 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
140function 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
240function displayJson(data, container) {
241  container.innerHTML = formatDataToJsonHTML(data);
242}
243
244function 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
255function Promise() {
256  if (!(this instanceof Promise)) { return new Promise(); }
257  this.paused = [];
258  this.satisfy = function () {
259    var paused = this.paused;
260console.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}
267Promise.prototype.toString = function () { return "Promise"; };
268function 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
292function newBlankObject() {
293  return (Object.create || Object)(null);
294}
295
296function getOwn(o, k, opt_default) {
297  return Object.hasOwnProperty.call(o, k) ? o[k] : opt_default;
298}
299
300function 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
325function 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
341function 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
359function 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
386function 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
394function inSet(s, str) {
395  return s[str] === s;
396}
397
398function elementContainsComment(el) {
399  return elementContainsNodeOfType(el, 8);
400}
401
402function elementContainsText(el) {
403  return elementContainsNodeOfType(el, 3);
404}
405
406function 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